wpf实现模块化开发
背景:
公司所在项目是管理软件的开发,针对不同客户,客户的不同岗位,所需的功能是完全不同的,可以理解成有的客户端需要100%的功能,有的客户端只需要10%的功能,所以针对不同岗位不同客户端进行插件化的模块化开发就十分重要了,加之客户的硬件非常差(xp电脑1g内存),所以内存的节约非常重要!
考虑到要在xp电脑上运行,所以只能上.net 4.0(不考虑在客户xp电脑运行.net4.5这种操作),像开源的prism这种本身就支持模块化的框架就不考虑了(主要是prism不支持.net4.0),最终选择自己搭建框架
需求:
- 插件化的,动态的加载模块.
- 模块与主程序之间不存在引用关系(缺少这个模块,程序不能报错)
- 除运行的主程序exe之外,其他的功能模块以动态类库dll的形式存在
- 方便对收费功能模块的管理(只有付费的,才会下发相应的dll)
实现:
- 主程序exe启动后,扫描根目录下module文件夹,加载module下所有的dll
- 通过反射,获取dll下所有的方法,将view,viewmodel加载到内存中
- 使用页面
public static Dictionary<string, VM> ViewModel = new Dictionary<string, VM>();
public static void Config() { //当前模块中的构造加载 AssemblyRegister(Assembly.GetExecutingAssembly()); //模块文件夹存在 var filePath = AppDomain.CurrentDomain.BaseDirectory + "/module/"; if (Directory.Exists(filePath)) { DirectoryInfo directoryInfo = new DirectoryInfo(filePath); var dlls = directoryInfo.GetFiles("*.dll", SearchOption.TopDirectoryOnly); //模块化模块中的加载 foreach (var dll in dlls) { if (File.Exists(dll.FullName)) { Assembly assembly = Assembly.LoadFrom(dll.FullName); AssemblyRegister(assembly); } } } }
AssemblyRegister(Assembly.GetExecutingAssembly());方法是在将主程序exe中的view,viewmodel也通过反射的方式加载到内存中
private static void AssemblyRegister(Assembly assembly) { foreach (Type type in assembly.GetTypes()) { if (type.Name == "ModuleConfig") { var obj = Activator.CreateInstance(type); //依赖注入方法 var method = type.GetMethod("Register"); method?.Invoke(obj, null); //菜单界面绑定方法 var viewModelBinding = type.GetMethod("ViewModel"); var ret = viewModelBinding?.Invoke(obj, null); if (ret != null && ret is Dictionary<string, VM> dic) { foreach (var item in dic) { if (!ViewModel.ContainsKey(item.Key)) ViewModel.Add(item.Key, item.Value); } } //automapper注入 var autoMapper = type.GetMethod("AutoMapper"); autoMapper?.Invoke(obj, null); } } }
注意这里的ModuleConfig,Register,ViewModel,AutoMapper,其中ModuleConfig是类名,也就是说每个模块中需要包含ModuleConfig这个类,并实现固定的方法,反射时才能通过方法名去加载,ModuleConfig去继承一个实现了这些方法的接口即可
public interface IModuleConfig { void Register(); void AutoMapper(); Dictionary<string, VM> ViewModel(); }
public class VM { public VM(Type V,Type M,string viewName = null) { VName = V.Name; MName = M.Name; this.V = V; this.M = M; ViewName = viewName; } public VM(Type V ,string viewName = null) { VName = V.Name; this.V = V; ViewName = viewName; } /// <summary> /// 视图 /// </summary> public string VName { get; set; } /// <summary> /// ViewModel /// </summary> public string MName { get; set; } /// <summary> /// 视图自定义名字 /// </summary> public string ViewName { get; set; } /// <summary> /// View /// </summary> public Type V { get; set; } /// <summary> /// ViewModel /// </summary> public Type M { get; set; } }
public class ModuleConfig : IModuleConfig { public void AutoMapper() { Mapper.CreateMap<MenuItemModel, UI.Controls.MenuItems>() .ForMember(x => x.MenuCode, y => y.MapFrom(z => z.ResourceNo)) .ForMember(x => x.MenuName, y => y.MapFrom(z => z.ResourceName)) .ForMember(dest => dest.Children, opt => opt.MapFrom(src => src.ChildrenList.Select(x => Mapper.Map<UI.Controls.MenuItems>(x)).ToList())) .ForMember(x => x.StringIcon, y => y.MapFrom(z =>string.IsNullOrEmpty(z.IconUrl)? "":z.IconUrl)); } public void Register() { //接口调用注入 SimpleIoc.Default.Register<IUserService, UserService>(); SimpleIoc.Default.Register<ISellTicketBaseService, SellTicketBaseService>(); SimpleIoc.Default.Register<IBaseDataService, BaseDataService>(); SimpleIoc.Default.Register<ISystemSetService, SystemSetService>(); SimpleIoc.Default.Register<IBillService, BillService>(); /////////////////////////////////////////////////////////// //弹框注入 SimpleIoc.Default.Register<IBaseDialogService, BaseDialogService>(); SimpleIoc.Default.Register<IDialogService, DialogService>(); //ViewModel注入 SimpleIoc.Default.Register<MainViewModel>(); SimpleIoc.Default.Register<LoginViewModel>(); SimpleIoc.Default.Register<FooterViewModel>(); SimpleIoc.Default.Register<TicketNoViewModel>(); SimpleIoc.Default.Register<ParameterSetViewModel>(); SimpleIoc.Default.Register<HomePageViewModel>(); } public Dictionary<string, VM> ViewModel() { return new Dictionary<string, VM> { {"xitongcanshupeizhi_81", new VM(typeof(ParameterSet),typeof(ParameterSetViewModel)) }, {"home_page", new VM(typeof(HomePage),typeof(HomePageViewModel)) }, }; } }
这里用了mvvmlight框架,所以Register方法直接往mvvmlight实现的方法注入我们的view,viewmodel
封装了AutoMapper,按需使用
ViewModel方法将界面菜单与界面绑定,用户点击某个菜单,直接路由到相应的界面
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { Current.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(App_DispatcherUnhandledException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); Log.Info("程序启动"); base.OnStartup(e); BaseModuleConfig.Config(); IUserService userService = SimpleIoc.Default.GetInstance<IUserService>(); CacheHelper.Dictionary = new Dictionary(userService); InitExtension.SystemInit(); //自动更新 LancherOrUpdate(userService,e.Args); } }
启动时调用BaseModuleConfig.Config();
/// <summary> /// 通过菜单code添加菜单 /// </summary> /// <param name="code"></param> public void AddMenu(string code,string name = null,object param = null) { VM vm = null; if (BaseModuleConfig.ViewModel.ContainsKey(code)) { vm = BaseModuleConfig.ViewModel[code]; } else { foreach (var item in BaseModuleConfig.ViewModel) { if (item.Value.VName == code) { vm = item.Value; break; } } } if (vm != null) { var viewType = vm.V; object viewmodel = null; if (vm.M != null)//指定了ViewModel { //通过框架注入 viewmodel = ServiceLocator.Current.GetInstance(vm.M); } if (viewType != null) { List<object> ctor = new List<object>();
//获取类的构造函数,为各个构造函数的签名赋值 var constructors = viewType.GetConstructors(); foreach (var con in constructors) { if (con.GetParameters().Length > 0) { foreach (var item in con.GetParameters()) { ctor.Add(SimpleIoc.Default.GetInstance(item.ParameterType)); } break; } } if(name!=null) vm.ViewName = name; var view = Activator.CreateInstance(viewType, ctor.ToArray()) as ContentControl; //BeforeAddMenu(view, viewmodel, new UI.Controls.MenuItems { MenuName = vm.ViewName, MenuCode = code }, vm); if (viewmodel is IViewModel model) { model.Load(param); } AddMenu(new MenuItem(view,viewmodel, new UI.Controls.MenuItems { MenuName = vm.ViewName, MenuCode = code })); } } }
手动实现依赖注入,为各个构造函数的签名赋值
这里的主程序exe不依赖于各个功能模块dll,所以只需根据用户需求和权限按需下发dll到根目录的module下即可.各个dll反而是依赖了主程序exe,我觉得这种设计思想也是完全可行的,模块化后降低了依赖提升了操作空间