wpf企业应用之UI模块解耦
关于UI模块的解耦,说简单点,首先需要配置菜单与对应操作类的映射关系(或存放于配置文件,或继承接口直接写死在模块代码中,或存放到数据库,原理都一样),然后在菜单加载时,读取配置项动态生成菜单或是其他控件列表,同时为对应菜单项添加点击之类的事件,最后在事件中利用反射生成模块的实例(与界面相关的还需加到父容器中)。
下面就我写的部分代码做一说明。具体效果见 wpf企业级开发中的几种常见业务场景
首先界面放置两个容器,一个放菜单,一个放模块UI。其中avalonDock是一个布局容器,可实现类似VS的布局方式。
接下来就是填充菜单,也就是动态生成菜单。
private void LoadMenu() { var modules = ModuleHelper.GetModuleInfo(); var menuitems = BuildMenu(modules); menuitems.ForEach(item => ModuleMenu.Items.Add(item)); }
先获取菜单,下面是菜单类及部分菜单配置示例。
public class ModuleInfo { public ModuleInfo() { } public string MenuName { get; set; } public string MenuName_EN { get; set; } public string AssemblyFile { get; set; } public bool CanSetRight { get; set; } public bool NotUse { get; set; } public string ClassName { get; set; } public string StartMethod { get; set; } private List<ModuleInfo> _moduleChildren; public List<ModuleInfo> ModuleChildren { get { return _moduleChildren ?? (_moduleChildren = new List<ModuleInfo>()); } set { _moduleChildren = value; } } }
基本上所有的注入都至少要配置菜单名、类名及操作方法名。
<Modules> <Module MenuName="系统" MenuName_EN="System" CanSetRight="true"> <Module MenuName="语言" MenuName_EN="Language"> <Module MenuName="中文" MenuName_EN="中文" ClassName="XMIS.Modules.MISSystem.LanguageSetting" StartMethod="SetChinese"> </Module> <Module MenuName="English" MenuName_EN="English" ClassName="XMIS.Modules.MISSystem.LanguageSetting" StartMethod="SetEnglish"> </Module> </Module> <Module MenuName="操作日志" MenuName_EN="ActionLog" ClassName="XMIS.Modules.MISSystem.ActionLog" CanSetRight="true"> </Module> <Module MenuName="退出" MenuName_EN="Exit" ClassName="XMIS.ShellForm" StartMethod="Exit"> </Module> </Module> <Module MenuName="产品" MenuName_EN="Product" CanSetRight="true"> <Module MenuName="产品类别" MenuName_EN="ProductClassify" ClassName="XMIS.Modules.Product.ProductClassify" CanSetRight="true"> </Module> <Module MenuName="产品列表" MenuName_EN="ProductList" ClassName="XMIS.Modules.Product.ProductList" CanSetRight="true"> </Module> <Module MenuName="产品配料" MenuName_EN="ProductPlan" ClassName="XMIS.Modules.Product.ProductMaterial" CanSetRight="true"> </Module> </Module> </Modules>
下面的两个方法读取菜单配置生成对应类。
public class ModuleHelper { private static List<ModuleInfo> BuildModel(XmlNodeList nodes) { var result = new List<ModuleInfo>(); if (nodes == null || nodes.Count == 0) return result; foreach (XmlNode node in nodes) { if (node.Attributes["NotUse"] != null && node.Attributes["NotUse"].Value == "true") continue; var model = new ModuleInfo(); if (node.Attributes["MenuName"] != null) model.MenuName = node.Attributes["MenuName"].Value; if (node.Attributes["MenuName_EN"] != null) model.MenuName_EN = node.Attributes["MenuName_EN"].Value; if (node.Attributes["AssemblyFile"] != null) model.AssemblyFile = node.Attributes["AssemblyFile"].Value; if (node.Attributes["ClassName"] != null) model.ClassName = node.Attributes["ClassName"].Value; if (node.Attributes["StartMethod"] != null) model.StartMethod = node.Attributes["StartMethod"].Value; if (node.Attributes["CanSetRight"] != null) model.CanSetRight = Convert.ToBoolean(node.Attributes["CanSetRight"].Value); model.ModuleChildren.AddRange(BuildModel(node.ChildNodes)); result.Add(model); } return result; } public static List<ModuleInfo> GetModuleInfo() { if (File.Exists("ModuleConfig.xml")) { XmlDocument doc = new XmlDocument(); doc.Load("ModuleConfig.xml"); var root = doc.DocumentElement; var modules = BuildModel(root.ChildNodes); return modules; } return null; } }
接下来在界面动态创建菜单项。
private List<MenuItem> BuildMenu(List<ModuleInfo> modules) { var menuitems = new List<MenuItem>(); if (modules == null || modules.Count == 0) return menuitems; foreach (var module in modules) { MenuItem menuItem = new MenuItem(); if (AppSetting.GetValue("language") == "en_us") menuItem.Header = module.MenuName_EN; else menuItem.Header = module.MenuName; menuItem.Tag = module; bool hasRight = HasModuleRight(module.ClassName); if (module.CanSetRight && !hasRight) menuItem.IsEnabled = false; if (!string.IsNullOrEmpty(module.ClassName)) menuItem.Click += menuItem_Click; var children = BuildMenu(module.ModuleChildren); children.ForEach(item => menuItem.Items.Add(item)); menuitems.Add(menuItem); } return menuitems; }
菜单创建后,接下来就是加载对应模块了。在菜单点击事件中使用反射调用对应类的对应方法。
//在此使用反射,根据程序集、类型及方法执行相应操作 private void menuItem_Click(object sender, RoutedEventArgs e) { ModuleInfo module = (sender as MenuItem).Tag as ModuleInfo; if (string.IsNullOrEmpty(module.AssemblyFile))//本程序集 { LoadModule(module); return; } //以下调用插件 var assembly = Assembly.Load(module.AssemblyFile); if (assembly != null) { try { var moduleInstance = assembly.CreateInstance(module.ClassName); moduleInstance.GetType().InvokeMember(module.StartMethod, BindingFlags.Default | BindingFlags.InvokeMethod, null, moduleInstance, null); } catch { MessageBox.Show(LanguageHelper.GetString("ShellForm_menuItem_Click_Msg1") + module.ClassName); } } }
填充UI容器,我这里的一些逻辑实现tab页的添加,同时tab页的标题及可以打开的数量可以在对应模块上进行配置,仅供参考,读者可根据自己实际情况编写逻辑。
private void LoadModule(ModuleInfo module) { try { Object moduleInstance = null; var showAttr = Type.GetType(module.ClassName).GetCustomAttribute<ModuleShowAttribute>(); int tabCount = ModuleManager.GetTabCount(module.ClassName); if (module.ClassName == "XMIS.ShellForm")//主窗体 moduleInstance = this; else { if (showAttr == null || tabCount < showAttr.MaxTabCount) { moduleInstance = Activator.CreateInstance(Type.GetType(module.ClassName)); ModuleManager.AddModuleTab(module.ClassName); } else//不再添加该模块标签页,直接激活 { var activeDoc = ModuleContainer.Children.FirstOrDefault(p => p.Content.GetType().ToString() == module.ClassName); if (activeDoc != null) activeDoc.IsSelected = true; return; } } if (moduleInstance == null) return; if (string.IsNullOrEmpty(module.StartMethod))//不配置StartMethod,就默认加载为标签页窗体 { var moduleTab = moduleInstance as Control; if (moduleTab != null) { var tabDoc = new LayoutDocument() { Content = moduleTab, IsSelected = true }; if (AppSetting.GetValue("language") == "en_us") tabDoc.Title = showAttr.ModuleName_EN; else tabDoc.Title = showAttr.ModuleName; tabDoc.Closed += tabDoc_Closed; tabDoc.IsSelectedChanged += tabDoc_IsSelectedChanged; ModuleContainer.Children.Add(tabDoc); } } else { moduleInstance.GetType().InvokeMember(module.StartMethod, BindingFlags.Default | BindingFlags.InvokeMethod, null, moduleInstance, null); } } catch { MessageBox.Show(LanguageHelper.GetString("ShellForm_LoadModule_Msg1") + module.ClassName); } }
到这里,注入容器基本完成了。下面这个类是我写的一个模块信息管理类,用于记录tab页的状况,方便进行一些特殊情况的处理。
public static class ModuleManager { private static Dictionary<string, int> _moduleTabDic = new Dictionary<string, int>(); public static Dictionary<string, int> ModuleTabDic { get { return _moduleTabDic; } set { _moduleTabDic = value; } } public static System.Windows.Window ContainerWindow { get; set; } public static void AddModuleTab(string moduleName) { if (ModuleTabDic.ContainsKey(moduleName)) ModuleTabDic[moduleName]++; else ModuleTabDic.Add(moduleName, 1); } public static int GetTabCount(string moduleName) { if (!ModuleTabDic.ContainsKey(moduleName)) return 0; return ModuleTabDic[moduleName]; } public static void RemoveModuleTab(string moduleName) { if (ModuleTabDic.ContainsKey(moduleName) && ModuleTabDic[moduleName] > 0) { ModuleTabDic[moduleName]--; } } public static void RemoveAllTab() { ModuleTabDic.Clear(); } private static MenuItem FindMenuItem(ItemCollection menuItems, string menuName) { foreach (var item in menuItems)//后根遍历搜索,之查找叶子节点,配置菜单时,菜单叶子节点名尽量不要重复 { var menuitem = (MenuItem)item; var temitem = FindMenuItem(menuitem.Items, menuName); if (temitem != null) return temitem; if (menuitem.Items.Count == 0 && ((ModuleInfo)menuitem.Tag).MenuName == menuName) return menuitem; } return null; } public static void SetMenuCheckState(string menuName, bool isChecked) { Menu ModuleMenu = (Menu)ContainerWindow.FindName("ModuleMenu"); var menuItem = FindMenuItem(ModuleMenu.Items, menuName); if (menuItem != null) menuItem.IsChecked = isChecked; } private static void SetMenuLanguage(ItemCollection menuItems, string language) { foreach (var item in menuItems) { var menuitem = (MenuItem)item; if (language == "en_us") menuitem.Header = ((ModuleInfo)menuitem.Tag).MenuName_EN; else menuitem.Header = ((ModuleInfo)menuitem.Tag).MenuName; SetMenuLanguage(menuitem.Items, language); } } public static void SetMenuLanguage(string language) { Menu ModuleMenu = (Menu)ContainerWindow.FindName("ModuleMenu"); SetMenuLanguage(ModuleMenu.Items, language); } public static void SetDocTitleLanguage(string language) { foreach (var module in ModuleTabDic.Keys) { var showAttribute = Type.GetType(module).GetCustomAttribute<ModuleShowAttribute>(); LayoutDocumentPane ModuleContainer = (LayoutDocumentPane)ContainerWindow.FindName("ModuleContainer"); var tab = ModuleContainer.Children.FirstOrDefault(m => m.Content.GetType().ToString() == module); if (tab == null) continue; if (language == "en_us") tab.Title = showAttribute.ModuleName_EN; else tab.Title = showAttribute.ModuleName; } } public static void SetStatusMessage(string message) { TextBlock Text_StatusMessage = (TextBlock)ContainerWindow.FindName("Text_StatusMessage"); Text_StatusMessage.Text = message; } }