首页 > 代码库 > wpf企业应用之UI模块解耦

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;            }        }    }
View Code

  基本上所有的注入都至少要配置菜单名、类名及操作方法名。

<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>
View Code

   下面的两个方法读取菜单配置生成对应类。

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 =http://www.mamicode.com/= "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;        }    }
View Code

   接下来在界面动态创建菜单项。

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;        }    }
View Code

wpf企业应用之UI模块解耦