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

时间: 2024-12-21 13:13:50

wpf企业应用之UI模块解耦的相关文章

React 组合式开发实践:打造企业管理系统五大核心模块

课程介绍:你会学到什么        从插件化到模块化再到组件化前端下一次的进化方向是什么        UI 组件越写越熟练项目的开发效率却并没有质的提升问题在哪里        如何使用组合式开发的思想拯救交互与体验都仍停留在上个世纪的企业管理系统        开发基于 React 16.0 & webpack 4.0 的前端项目脚手架        企业管理系统的通用页面布局方案        页面级别基于用户角色的前端权限系统        自动匹配当前路由的无限级菜单设计      

使用WPF来创建 Metro UI程序

本文转载:http://www.cnblogs.com/TianFang/p/3184211.html 这个是我以前网上看到的一篇文章,原文地址是:Building a Metro UI with WPF,这篇文章一步步的介绍了如何实现一个Metro样式的窗口,并且效果非常好.今天看到OSChina上有这篇文章的译文:使用WPF来创建 Metro UI,翻译得非常好,这里推荐一下. 值得一提的是,除了这种一步步来的方式外,现在Codeplex中也有不少比较好的metro风格的wpf框架,可以帮助

WPF中异步更新UI元素

XAML 界面很简单,只有一个按钮和一个lable元素,要实现点击button时,lable的内容从0开始自动递增. <Grid> <Label Name="lable_plus" Content="0"/> <Button Content="Button" Click="button_Click" Height="23" Name="button" Wid

WPF 支持的多线程 UI 并不是线程安全的

原文:WPF 支持的多线程 UI 并不是线程安全的 WPF 支持创建多个 UI 线程,跨窗口的或者窗口内的都是可以的:但是这个过程并不是线程安全的. 你有极低的概率会遇到 WPF 多线程 UI 的线程安全问题,说直接点就是崩溃.本文将讲述其线程安全问题. 此问题现已报告给微软:Creating multi-thread UI has a low probability to crash · Issue #297 · dotnet/wpf 本文内容 简述这个线程安全问题 复现步骤 简述这个线程安全

WPF 精修篇 非UI进程后台更新UI进程

原文:WPF 精修篇 非UI进程后台更新UI进程 <Grid> <Grid.RowDefinitions> <RowDefinition Height="11*"/> <RowDefinition Height="29*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Margin="0&quo

C# WPF 使用委托修改UI控件

近段时间在自学WPF,是一个完全不懂WPF的菜鸟,对于在线程中修改UI控件使用委托做一个记录,给自已以后查询也给需要的参考: 界面只放一个RichTextBox,在窗体启动时开起两个线程,调用两个函数,每隔1秒写一次当前时间 一 界面XAML如下: <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&qu

wpf企业应用之带选项框的TreeView

wpf里面实现层次绑定主要使用HierarchicalDataTemplate,这里主要谈一谈带checkbox的treeview,具体效果见 wpf企业级开发中的几种常见业务场景. 先来看一下我的控件绑定,我这里实现的是模块权限的编辑.具体效果就是选中一个节点,后代节点.祖代节点状态都会发生相应变化,具体变化逻辑大家都懂的,描述起来很罗嗦. <TreeView Name="TreeView_Right" ItemsSource="{Binding ModuleRigh

wpf企业应用之主从结构列表

主从结构在企业级应用中相当常见,这里结合我的例子谈一下wpf中主从结构列表展示的常用做法,具体效果见 wpf企业级开发中的几种常见业务场景. 首先,Model有两种,主表对应model(假设为modelA),从表对应的model(假设为modelB),两种model分别用于绑定列表,就是普通列表的绑定. 其次,由于要实现联动效果(即选择主表中的一条记录显示从表的记录),故而我们的ViewModel里面必须设计一个SelectedModelA用来绑定选中项,SelectedModelA变化时去更新

[WPF]WPF Data Virtualization和UI Virtualization

这篇博客将介绍WPF中的虚拟化技术. 1. Data Virtualization 通常情况下我们说数据虚拟化是指数据源没有完全加载,仅加载当前需要显示的数据呈现给用户.这种场景会让我们想到数据分页显示,当需要特定页面的数据时,根据页数请求相应数据. WPF没有提供对Data Virtualization原生态的支持,当时我们可以使用Paging相关技术来实现.在我先前的博客WPF 实现 DataGrid/ListView 分页控件中有介绍. 2. UI Virtualization 是针对数据