简介:这是一套基于 WPF 的 WMS 仓库管理系统示例源码,面向具备一定 C# 与 XAML 基础、希望学习企业级桌面端架构的开发者。项目采用 MVVM 模式,配合 Stylet 框架与 MaterialDesign 样式库,实现了登录界面、主界面、左侧下拉菜单以及 DataGrid 数据查询展示等典型模块,数据库使用 SqlServer2008,并通过 SqlSugar 框架完成数据访问,适合作为仓库管理类项目的入门参考或二次开发骨架。压缩包为 rar 格式,共 498 个文件,约 17.8MB,其中 140 个 cs 源码文件承载业务与视图模型逻辑,13 个 xaml 与 13 个 baml 构成界面布局,另有 67 个 dll 依赖库、20 个 json 配置及若干 props、targets 等工程文件,结构完整。目前已有 1072 人学习下载。读者可从中获取 MVVM 分层组织方式、Stylet 依赖注入与窗口管理、MaterialDesign 控件样式定制、SqlSugar 增删改查封装以及登录与主界面导航的完整实现思路,便于快速理解 WPF 仓库系统的工程化写法。
1. 从登录窗到主界面:一套 WPF MVVM WMS 骨架到底能省多少事
如果你接过仓库管理系统的私活,或者被派去维护一套跑了五六年的 WMS,大概率会遇到这种局面:登录窗里塞满了按钮点击事件,主界面左侧菜单是硬编码的 TreeView,DataGrid 的列在 XAML 里一行行手写,改一个字段要翻三个文件。这套基于 WPF MVVM 的 WMS 仓库管理骨架,解决的正是这个阶段的痛点——它把登录、主界面、左侧下拉菜单、DataGrid 查询展示这几块最容易写乱的部分,用 Stylet 的 MVVM 模式重新组织了一遍,样式层交给 MaterialDesign,数据访问走 SqlSugar,数据库落在 SqlServer2008 上。
它适合两类人:一是刚接触 WPF MVVM、想找一个能跑起来的完整参照的开发者;二是手里有 WMS 项目、需要快速搭出登录加主框架的从业者。资源里能看到 LoginView、MainWindowView、UserControlMenuItemView 这些视图文件,还有 StyleDic、MenuItemStyle 这类样式资源字典,说明它不是零散代码片段,而是一套有结构的分层骨架。下面我按实际拆包的顺序,把选型理由、落地步骤和踩过的坑一条条讲清楚。
2. Stylet 与 MaterialDesign 的选型账:为什么不用原生 MVVM
2.1 Stylet 相比原生 MVVM 省掉的样板代码
原生 WPF 做 MVVM,最烦的是 INotifyPropertyChanged 的实现。每个 ViewModel 都要继承一个基类,属性 setter 里手动写 OnPropertyChanged,命令还要自己实现 ICommand。Stylet 把这层全包了:ViewModel 继承 Screen 或 PropertyChangedBase,属性用public string UserName { get => Get(); set => Set(value); }这种写法,框架自动处理通知;命令直接用[Command]特性标注方法,连 RelayCommand 都不用 new。
这套骨架里 LoginView 和 MainWindowView 的绑定能保持干净,靠的就是 Stylet 的这套约定。我一般会先确认项目里 Stylet 的版本,因为不同版本对[Command]的 CanExecute 写法有差异。常见做法是在 App.xaml.cs 里用 Bootstrapper 启动,把根 ViewModel 设为 MainWindowViewModel,Stylet 会自动完成 View 和 ViewModel 的绑定,命名规则是 View 去掉 View 后缀加 ViewModel。
// App.xaml.cs 启动配置 public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // Stylet 的 Bootstrapper 负责 View-ViewModel 自动绑定 var bootstrapper = new Bootstrapper(); bootstrapper.Start(this); } }这段代码的关键在于 Bootstrapper 的启动时机。如果放在 OnStartup 里但没调 base.OnStartup,Stylet 的容器初始化会不完整,表现为 View 找不到对应的 ViewModel,界面一片空白。参数上没什么可调的,但要注意 Bootstrapper 默认从当前程序集扫描,如果 ViewModel 放在独立类库,需要重写 Configure 方法手动注册。
2.2 MaterialDesign 的样式接入与资源字典顺序
MaterialDesign 在 WPF 里的接入方式是把它的资源字典合并进 App.xaml。这套骨架里 StyleDic.baml、MenuItemStyle.baml 这些文件,说明作者已经把部分样式抽成了独立字典。合并顺序很讲究:MaterialDesign 的基础字典要在前,自定义的 StyleDic 在后,否则自定义样式会被覆盖。
<!-- App.xaml 资源合并顺序 --> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <!-- 先加载 MaterialDesign 基础样式 --> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" /> <!-- 再加载项目自定义样式,覆盖优先级更高 --> <ResourceDictionary Source="Styles/StyleDic.xaml" /> <ResourceDictionary Source="Styles/MenuItemStyle.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>逻辑说明:MergedDictionaries 里后加载的字典优先级更高,所以自定义样式放后面才能覆盖 MaterialDesign 的默认值。参数上要注意 Source 的写法,用 pack URI 时程序集名称和路径大小写必须完全匹配,否则运行时报找不到资源。我见过有人把 Styles 写成 styles,设计器里能预览,运行时直接崩,这种玄学问题查起来很费时间。
2.3 SqlSugar 在 SqlServer2008 上的连接配置
数据库层用 SqlSugar,连接 SqlServer2008 时要注意驱动兼容性。SqlSugar 默认走 System.Data.SqlClient,2008 版本对加密连接的支持有限,连接字符串里如果带了 Encrypt=True 会连不上。常见做法是显式关掉加密,并指定超时时间。
// SqlSugar 客户端初始化 public static SqlSugarClient GetInstance() { var db = new SqlSugarClient(new ConnectionConfig { // SqlServer2008 建议关闭加密,否则握手可能失败 ConnectionString = "Server=.;Database=WmsDb;User Id=sa;Password=***;Encrypt=False;", DbType = DbType.SqlServer, IsAutoCloseConnection = true, // 自动释放连接,避免连接池耗尽 InitKeyType = InitKeyType.Attribute // 从实体特性读取主键 }); return db; }参数说明:IsAutoCloseConnection 设为 true 后,每次查询完自动关闭,适合 WMS 这种高频短查询场景;如果设 false,需要手动 using 或 Dispose,否则连接池很快被打满。InitKeyType 用 Attribute 时,实体类的主键要标[SugarColumn(IsPrimaryKey = true)],不然插入时 SqlSugar 不知道哪个字段是自增主键,会报错。SqlServer2008 不支持 OFFSET FETCH 分页语法,SqlSugar 会自动降级成 ROW_NUMBER 方案,这点不用手动处理,但分页查询的 SQL 日志里能看到差异。
3. 登录到主界面的完整链路:从 LoginView 到 MainWindowView
3.1 登录验证与窗口跳转的 MVVM 写法
登录流程最容易写歪的地方是在 View 的代码后置里直接 new 主窗口然后 Show。MVVM 下应该由 ViewModel 发命令,通过 Stylet 的 IWindowManager 来切换。这套骨架里 LoginView 和 MainWindowView 是分开的,登录成功后要关掉登录窗、打开主窗。
// LoginViewModel 中的登录命令 [Command] public void Login() { // 基础校验,避免空查询打到数据库 if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) { MessageBox.Show("账号或密码不能为空"); return; } // SqlSugar 查询用户,密码实际项目应做哈希比对 var user = db.Queryable<SysUser>() .Where(u => u.Account == UserName && u.Password == Password) .First(); if (user == null) { MessageBox.Show("账号或密码错误"); return; } // 通过 Stylet 的窗口管理器打开主界面 var windowManager = IoC.Get<IWindowManager>(); windowManager.ShowWindow(new MainWindowViewModel()); // 关闭当前登录窗 RequestClose(); }逻辑说明:RequestClose 是 Stylet 的 Screen 基类方法,调用后登录窗关闭。参数上要注意 ShowWindow 和 ShowDialog 的区别,ShowWindow 是非模态,主界面打开后登录窗还能操作;ShowDialog 是模态,会阻塞。WMS 场景一般用 ShowWindow,但要在打开主窗前先 RequestClose,否则两个窗口同时存在,用户关掉主窗后登录窗还挂着,进程退不出去。我一般会在主窗的 OnClose 里调 Application.Current.Shutdown,确保进程干净退出。
3.2 左侧下拉菜单的数据绑定与 MenuItemStyle
左侧菜单是 WMS 的导航核心。这套骨架里有 UserControlMenuItemView 和 MenuItemStyle,说明菜单项被抽成了独立控件加样式。菜单数据一般从数据库的菜单表读,绑定到 ItemsControl 或 TreeView。用 ItemsControl 加 Expander 做下拉菜单是常见做法,比 TreeView 更容易控制样式。
<!-- 左侧菜单容器,绑定 MenuItems 集合 --> <ItemsControl ItemsSource="{Binding MenuItems}"> <ItemsControl.ItemTemplate> <DataTemplate> <!-- 每个菜单项用 Expander 实现下拉 --> <Expander Header="{Binding MenuName}"> <ItemsControl ItemsSource="{Binding Children}"> <ItemsControl.ItemTemplate> <DataTemplate> <!-- 子菜单点击触发导航命令 --> <Button Content="{Binding MenuName}" Command="{Binding DataContext.NavigateCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding MenuUrl}" Style="{StaticResource MenuItemStyle}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Expander> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>逻辑说明:外层 ItemsControl 绑顶级菜单,内层绑子菜单。命令绑定用 RelativeSource 找到外层 DataContext,因为子菜单的 DataContext 已经变成子项对象了,直接写 Command 会找不到。参数上 CommandParameter 传 MenuUrl,导航时根据这个值决定加载哪个 UserControl。MenuItemStyle 里一般会设 Expander 的展开箭头、选中高亮和缩进,MaterialDesign 的默认 Expander 样式偏圆润,WMS 场景通常要改成紧凑型,把 Padding 和 Margin 调小。
3.3 DataGrid 查询展示与分页参数
DataGrid 是 WMS 里用得最多的控件,查询展示要处理分页、排序和列宽。这套骨架的 DataGrid 绑定的是 ViewModel 里的集合,查询走 SqlSugar 的分页方法。SqlServer2008 下分页要注意总条数查询和当前页数据查询要分开,SqlSugar 的 ToPageList 会自动处理。
// 分页查询命令 [Command] public void Query() { int total = 0; // 每页 20 条,按创建时间倒序 var list = db.Queryable<StockInfo>() .WhereIF(!string.IsNullOrEmpty(Keyword), s => s.Name.Contains(Keyword)) .OrderBy(s => s.CreateTime, OrderByType.Desc) .ToPageList(PageIndex, PageSize, ref total); // 更新集合和总条数 StockList = new ObservableCollection<StockInfo>(list); TotalCount = total; }逻辑说明:WhereIF 是 SqlSugar 的条件拼接,Keyword 为空时不加这个条件,避免拼出WHERE Name LIKE '%%'这种低效查询。参数上 PageIndex 从 1 开始,PageSize 建议 20 到 50,太大在 SqlServer2008 上 ROW_NUMBER 排序开销明显。ToPageList 的 ref total 返回总条数,用来算总页数。注意 ObservableCollection 每次查询都 new 一个新的,不要用 Clear 再 Add,后者会触发多次 CollectionChanged,大数据量下界面卡顿。
4. 避坑排查:这套骨架跑不起来时先看这几处
4.1 启动后界面空白,View 找不到 ViewModel
现象:程序能启动,但主窗口一片空白,没有报错。原因:Stylet 的命名约定没匹配上,View 叫 MainWindowView,ViewModel 必须叫 MainWindowViewModel,且在同一程序集或已注册的命名空间下。解决:检查 View 和 ViewModel 的后缀,确认 Bootstrapper 扫描的程序集包含 ViewModel;如果 ViewModel 在类库,重写 Configure 方法手动绑定。
4.2 MaterialDesign 样式不生效,控件还是原生样子
现象:按钮、文本框还是 WPF 默认样式,MaterialDesign 的圆角和颜色没出来。原因:资源字典合并顺序错了,或者控件的 Style 没显式引用 MaterialDesign 的样式键。解决:确认 App.xaml 里 MaterialDesign 字典在前、自定义字典在后;控件上要写Style="{StaticResource MaterialDesignRaisedButton}"这类显式引用,MaterialDesign 不会自动应用到所有控件。
4.3 SqlSugar 连接 SqlServer2008 报握手失败
现象:本地能连,部署到服务器报连接超时或握手错误。原因:连接字符串带了 Encrypt=True,SqlServer2008 的 TLS 版本不支持。解决:连接字符串加Encrypt=False,如果还不行,检查服务器是否开了 TCP/IP 协议,2008 默认可能只开命名管道。
4.4 DataGrid 分页查询第二页数据重复
现象:翻到第二页,数据和第一页一样。原因:排序字段不唯一,ROW_NUMBER 在相同值上顺序不稳定。解决:OrderBy 里加一个唯一字段做次级排序,比如OrderBy(s => s.CreateTime, Desc).OrderBy(s => s.Id),确保每页数据不重叠。
4.5 登录窗关闭后进程不退出
现象:关掉主窗口,任务管理器里进程还在。原因:登录窗用了 ShowDialog 或者主窗关闭时没调 Shutdown。解决:登录用 ShowWindow,主窗的 OnClose 里调Application.Current.Shutdown(),同时确认没有后台线程或定时器在跑。
5. 菜单权限与 DataGrid 列配置的进阶处理
菜单权限这块,骨架里没直接给,但实际 WMS 项目绕不开。我一般会在登录成功后,根据用户角色从菜单表里过滤出有权限的菜单项,再绑定到左侧 ItemsControl。菜单表设计成自关联结构,ParentId 为 0 的是顶级,查询时递归或一次性拉全表在内存里组树。SqlSugar 没有内置递归查询,SqlServer2008 也不支持 CTE 递归的某些写法,常见做法是一次性查出所有菜单,在 C# 里用字典组树,数据量几百条以内性能没问题。
// 一次性查全量菜单,内存组树 var allMenus = db.Queryable<SysMenu>().Where(m => m.IsEnable).ToList(); // 按 ParentId 分组,方便递归取子节点 var lookup = allMenus.ToLookup(m => m.ParentId); // 递归构建树 List<MenuNode> BuildTree(int parentId) { return lookup[parentId].Select(m => new MenuNode { MenuName = m.MenuName, MenuUrl = m.MenuUrl, Children = BuildTree(m.Id) // 递归取子菜单 }).ToList(); } var tree = BuildTree(0);这段的关键是 ToLookup,它把列表转成按 ParentId 索引的查找表,递归时直接 lookup[parentId] 取子节点,比每次遍历全表快得多。参数上 IsEnable 过滤掉停用的菜单,实际项目还要加角色过滤,在 Where 里拼角色条件。递归深度一般不超过三层,WMS 菜单很少更深,如果超过三层要考虑改成懒加载。
DataGrid 的列配置,如果列是固定的,直接在 XAML 里写 DataGridTextColumn 就行;但 WMS 里经常有用户自定义列显示的需求,这时候要用动态列。常见做法是 ViewModel 里维护一个列配置集合,用 DataGrid 的 Columns 绑定,但 WPF 的 DataGrid.Columns 不是依赖属性,不能直接绑。替代方案是用附加属性或者在代码后置里根据配置动态生成列。我一般会在 View 的 Loaded 事件里读列配置,然后dataGrid.Columns.Add(new DataGridTextColumn { Header = cfg.Header, Binding = new Binding(cfg.Field) }),这样列配置存数据库,用户换台机器登录也能恢复布局。
验证这套骨架是否跑通,我习惯按这个顺序走一遍:先确认登录窗能弹出且 Stylet 绑定生效,再用错误密码试一次看提示是否正常,然后正确登录进主界面,检查左侧菜单是否从数据库读出、点击子菜单能否切换右侧内容,最后在 DataGrid 里翻两页确认分页和排序。每一步都过了,再往里面填业务逻辑。从那以后我每次拿到新的 WPF 骨架,都强制先跑通登录到主界面的最小闭环,再动其他代码,不然样式和绑定的问题混在一起,排查起来就是黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取