上一篇文章中,介绍了关于web api的封装事情,其实在做类似这种项目,也可以叫前后端分离的项目吧(客户端用WPF,服务器端是 web api),调用web api这块功能如果自己手工撸的话,确实麻烦,web api的URL(或者路由)开发阶段后端随时可能更改地址或增删参数等,封装后就基本上解决了体力活了,那接下来看看入口项目。
如果你在github拉取了源码,可以看到JHRS.Shell这个库就是WPF 客户端的入口项目,是整个系统的一个外壳程序,它会在启动后将各个子系统加载进来;采用这种分而治之的方式将一个大系统划分为多个子系统后,必然有一个入口程序来统一调度和加载各个子系统,凡是需要在各子系统外部实现的功能,都可以放到入口程序来处理,如自动升级功能。
WPF客户端入口项目
在上图中可以看到,JHRS框架中的目录并不多,而你要明白,这只是演示项目,真实项目中的要复杂得多,但这个框架基本功能是已经可以满足开发了,需要的功能,自己扩展就可以了;只要明白了为什么这样做,相信猿猿们可以搞定的。
在Shell库中,主要是做了这些事情
- App.config保存整个系统的配置信息
- App.xaml可以加载整个系统外部资源
- ShellSwitcher封装了关闭,打开窗口功能
- Views根目录定义了主窗体,主窗体里面布局好各个区域
- Login里面放的是登录相关的窗体和页面,用于完成登录进入系统
- Dialogs定义了整个系统里面统一使用的模态窗口,消息提示框的展示(调用实现封装到ViewModel基类里面的,因为ViewModel里面做了业务需要各种提示)
- ViewModels里面则是入口程序各个页面(Page)或窗体(Window)的后台业务逻辑
- images目录里面是需要用到的一些图片,你也可以移到外部资源里面;图片也可以使用svg格式
统一加载外部资源
<prism:PrismApplication x:Class="JHRS.Shell.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:JHRS.Shell" xmlns:prism="http://prismlibrary.com/"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/JHRS.Wpf;component/Style/TextBoxStyle.xaml" /> <ResourceDictionary Source="/JHRS.Wpf;component/Style/ButtonStyle.xaml" /> <ResourceDictionary Source="/JHRS.Wpf;component/Style/PageIcon.xaml" /> <ResourceDictionary Source="/JHRS.Wpf;component/Style/ComboBox.xaml" /> <ResourceDictionary Source="/JHRS.Wpf;component/Resources/ConverterResources.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Lime.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </prism:PrismApplication>
这是WPF加载外部资源的标准操作,像ConverterResources.xaml里面就是在外部定义的WPF需要使用到的Converter(数据转换器),各子系统也可以使用。
统一消息提示框和模态窗口
在入口项目中,Views/Dialogs里面可以看到,定义了Alert,Confirm以及DialogPage等,并且要实现这些功能,还需要在启动类(App.xaml.cs)里面注册对话框服务。完整的代码如下所示:
using JHRS.Core.Modules; using JHRS.Reflection; using JHRS.Shell.ViewModels.Dialogs; using JHRS.Shell.Views.Dialogs; using JHRS.Shell.Views.Login; using Prism.Ioc; using Prism.Modularity; using Prism.Regions; using Prism.Services.Dialogs; using Prism.Unity; using System; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using Unity; namespace JHRS.Shell { public partial class App : PrismApplication { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); DispatcherUnhandledException += App_DispatcherUnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } protected override Window CreateShell() { return Container.Resolve<LoginWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterSingleton<PageManager>(); containerRegistry.RegisterSingleton<UserControlManager>(); Type[] pages = AppDomainAllAssemblyFinder.FindAll<Page>(); var pageManager = containerRegistry.GetContainer().Resolve<PageManager>(); Type[] array = pages; foreach (Type item in array) { containerRegistry.RegisterForNavigation(item, item.FullName); FunctionAttribute function = item.GetAttribute<FunctionAttribute>(); if (function != null) { pageManager.Add(function.UniqueName, item); } } Type[] controls = AppDomainAllAssemblyFinder.FindAll<UserControl>(); var controlManager = containerRegistry.GetContainer().Resolve<UserControlManager>(); Type[] array2 = controls; foreach (Type item2 in array2) { containerRegistry.RegisterForNavigation(item2, item2.FullName); QueryLocatorAttribute locator = item2.GetAttribute<QueryLocatorAttribute>(); if (locator != null) { controlManager.Add(item2.FullName, new ControlMapping { ControlType = item2, RegionName = locator.RegionName, TargetType = locator.Target }); } } containerRegistry.RegisterDialog<AlertDialog, AlertDialogViewModel>(); containerRegistry.RegisterDialog<ConfirmDialog, ConfirmDialogViewModel>(); containerRegistry.RegisterDialog<ErrorDialog, ErrorDialogViewModel>(); containerRegistry.RegisterDialog<SuccessDialog, SuccessDialogViewModel>(); containerRegistry.RegisterDialog<WarningDialog, WarningDialogViewModel>(); containerRegistry.Register(typeof(IDialogWindow), typeof(Views.Dialogs.DialogWindow), "dialog"); containerRegistry.RegisterDialog<CommonDialogPage, CommonDialogPageViewModel>(); } protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); } /// <summary> /// 注册系统模块 /// </summary> /// <param name="moduleCatalog"></param> protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { var modules = AppDomainAllAssemblyFinder.FindAll<IModule>(); foreach (var item in modules) { moduleCatalog.AddModule(new ModuleInfo { ModuleName = item.Name, ModuleType = item.AssemblyQualifiedName, InitializationMode = InitializationMode.OnDemand }); } } protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); } protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @".\Modules" }; } /// <summary> /// 统一异常处理(非UI线程未捕获异常处理事件(例如自己创建的一个子线程)) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Exception ex = e.ExceptionObject as Exception; if (ex != null) { MessageBox.Show($"程序组件出错,原因:{ex.Message}", "系统提示", MessageBoxButton.OK, MessageBoxImage.Error); } } /// <summary> /// 统一异常处理(UI线程未捕获异常处理事件) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { Exception ex = e.Exception; MessageBox.Show($"程序运行出错,原因:{ex.Message}-{ex.InnerException?.Message}", "系统提示", MessageBoxButton.OK, MessageBoxImage.Error); e.Handled = true;//表示异常已处理,可以继续运行 } /// <summary> /// 统一异常处理(Task任务异常) /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { Exception ex = e.Exception; MessageBox.Show($"执行任务出错,原因:{ex.Message}", "系统提示", MessageBoxButton.OK, MessageBoxImage.Error); } } }
本项目中使用Prism,自定义弹框需要实现IDialogAware接口,更加详细的内容可以参考源码或者阅读这篇.NET 5 WPF MVVM框架Prism对话框服务博客文章。
主窗口
JRHS框架最初搭建来是开发HIS客户端的,先来看看主窗口长什么样子,当然这只是演示的一个项目,并非真实的HIS系统,真实项目中的HIS系统比这个复杂得多,布局上也是传统派风格,顶部菜单,下方Tab选项卡展示内容,这种风格的布局也是传统的增删改查项目所爱用的。
主窗口的xaml文件
<Window x:Class="JHRS.Shell.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:cmmod="clr-namespace:JHRS.Core.Modules;assembly=JHRS.Core" xmlns:b="http://schemas.microsoft.com/xaml/behaviors" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:prism="http://prismlibrary.com/" xmlns:local="clr-namespace:JHRS.Shell.Views" x:Name="main" prism:ViewModelLocator.AutoWireViewModel="True" WindowStartupLocation="CenterScreen" Icon="/images/jhrs.ico" Title="江湖郎中管理系統【首发于:jhrs.com】" Height="768" Width="1200"> <Window.Resources> <ResourceDictionary> <Color x:Key="ForeReverseColor">#FFFFFF</Color> <Color x:Key="MainColor">green</Color> <SolidColorBrush x:Key="DeepForegroundBrush" Color="#e0e0e0" /> <SolidColorBrush x:Key="ForeReverseBrush.OpacityTwo" Opacity="0.2" Color="{StaticResource ForeReverseColor}" /> <SolidColorBrush x:Key="ForeReverseBrush.OpacitySix" Opacity="0.6" Color="{StaticResource ForeReverseColor}" /> <SolidColorBrush x:Key="MainBrush" Color="{StaticResource MainColor}" /> <SolidColorBrush x:Key="ForeReverseBrush" Color="{StaticResource ForeReverseColor}" /> </ResourceDictionary> </Window.Resources> <b:Interaction.Triggers> <b:EventTrigger EventName="Closing"> <b:InvokeCommandAction PassEventArgsToCommand="True" Command="{Binding CloseWindowCommand}" /> </b:EventTrigger> </b:Interaction.Triggers> <Grid> <Grid.RowDefinitions> <RowDefinition Height="60" /> <RowDefinition /> </Grid.RowDefinitions> <Grid Background="#FF008000"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto" /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Image Source="/images/mainlogo.png" HorizontalAlignment="Center" /> <Menu Name="MainMenu" Grid.Column="1" Margin="0,0,220,0" ItemsSource="{Binding Path=MainMenuItemsSource}"> <ItemsControl.ItemContainerStyleSelector> <local:MenuStyleSelector /> </ItemsControl.ItemContainerStyleSelector> <Menu.Resources> <ResourceDictionary> <Style x:Key="MainMenuStyle" TargetType="{x:Type MenuItem}"> <Setter Property="Foreground" Value="#FFFFFF" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="Padding" Value="23,0" /> <Setter Property="FontSize" Value="14" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type MenuItem}"> <Grid> <Grid Name="MenuContentBorder" Background="{TemplateBinding Background}"> <Rectangle Name="SelectedBackground" Fill="{StaticResource ForeReverseBrush.OpacityTwo}" /> <ContentPresenter ContentSource="Header" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Margin="{TemplateBinding Padding}" /> </Grid> <Popup PopupAnimation="Slide" AllowsTransparency="True" IsOpen="{Binding Path=IsSubmenuOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"> <Border Background="#FFFFFFFF" BorderBrush="{StaticResource DeepForegroundBrush}"> <ItemsPresenter /> </Border> </Popup> </Grid> <ControlTemplate.Triggers> <Trigger Property="UIElement.IsMouseOver" Value="True"> <Setter TargetName="SelectedBackground" Property="Visibility" Value="Visible" /> </Trigger> <Trigger Property="UIElement.IsMouseOver" Value="False"> <Setter TargetName="SelectedBackground" Property="Visibility" Value="Hidden" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="UIElement.IsMouseOver" Value="True"> <Setter Property="Foreground" Value="{StaticResource ForeReverseBrush}" /> <Setter Property="FontWeight" Value="Bold" /> </Trigger> </Style.Triggers> </Style> <Style x:Key="{x:Type MenuItem}" TargetType="{x:Type MenuItem}"> <Style.Triggers> <Trigger Property="UIElement.IsMouseOver" Value="true"> <Setter Property="Foreground" Value="{StaticResource ForeReverseBrush}" /> </Trigger> </Style.Triggers> <Setter Property="Foreground" Value="{StaticResource ForeReverseBrush.OpacitySix}" /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="FontWeight" Value="Normal" /> <Setter Property="FontSize" Value="12" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type MenuItem}"> <Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"> <Grid Name="SelectedBackground"> <ContentPresenter Margin="{TemplateBinding Padding}" ContentSource="Header" ContentTemplate="{TemplateBinding HeaderTemplate}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/> <b:Interaction.Triggers> <b:EventTrigger EventName="MouseLeftButtonUp" > <b:InvokeCommandAction Command="{Binding DataContext.SelectedIntoPage,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" CommandParameter="{Binding .}" /> </b:EventTrigger> </b:Interaction.Triggers> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="UIElement.IsMouseOver" Value="True"> <Setter TargetName="SelectedBackground" Property="Panel.Background" Value="{StaticResource ForeReverseBrush.OpacityTwo}" /> </Trigger> <Trigger Property="UIElement.IsMouseOver" Value="False"> <Setter TargetName="SelectedBackground" Property="Panel.Background" Value="{x:Null}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="MinWidth" Value="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=MenuItem}}" /> <Setter Property="Height" Value="36" /> <Setter Property="Padding" Value="10,0" /> <Setter Property="Background" Value="{StaticResource MainBrush}" /> </Style> <HierarchicalDataTemplate x:Key="{DataTemplateKey {x:Type cmmod:MenuEntity}}" DataType="{x:Type cmmod:MenuEntity}" ItemsSource="{Binding Path=Children}"> <TextBlock Text="{Binding Path=Name}" /> </HierarchicalDataTemplate> </ResourceDictionary> </Menu.Resources> <Menu.Style> <Style TargetType="{x:Type Menu}"> <Setter Property="Background" Value="#00FFFFFF" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Menu}"> <ItemsPresenter Name="ItemsPresenter" /> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </Setter.Value> </Setter> </Style> </Menu.Style> </Menu> <ItemsControl HorizontalAlignment="Right" Grid.Column="2"> <TextBlock FontSize="24" Foreground="#FFFFFFFF" Margin="2" HorizontalAlignment="Right" Text="{Binding CurrentUser.HospitalName}" /> <TextBlock FontSize="14" Foreground="#FFFFFFFF" Margin="1" Text="{Binding CurrentUser.ShowText}" /> </ItemsControl> </Grid> <TabControl Name="MainTabPanel" BorderBrush="#FFDCDCDC" Grid.Row="1" Margin="5" BorderThickness="0"> <TabControl.Resources> <ResourceDictionary> <Style x:Key="{x:Type TabItem}" TargetType="{x:Type TabItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TabItem}"> <Border Name="Border" Height="30" BorderThickness="1,1,1,0" BorderBrush="#FFDCDCDC" CornerRadius="4,4,0,0" Margin="3,0"> <ContentPresenter Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center" ContentSource="Header" Margin="20,2" /> </Border> <ControlTemplate.Triggers> <Trigger Property="TabItem.IsSelected" Value="True"> <Setter TargetName="Border" Property="Border.Background" Value="#FF87CEFA" /> </Trigger> <Trigger Property="TabItem.IsSelected" Value="False"> <Setter TargetName="Border" Property="Border.Background" Value="#FFF8F8FF" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary> </TabControl.Resources> </TabControl> </Grid> </Window>
如果要做得更精美,还得需要美工来努力,这个演示窗体这么难看,还真不好意思拿出来,但不拿出来瞅瞅样子呢,也不好交待。
主窗口后置的C#代码
using CommonServiceLocator; using JHRS.Core.Events; using JHRS.Core.Modules; using Prism.Events; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace JHRS.Shell.Views { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); PageEvent pageEvent = ServiceLocator.Current.TryResolve<IEventAggregator>().GetEvent<PageEvent>(); pageEvent.Subscribe((p) => { MenuEntity menu = p.Menu; AddPage(menu.Name, p.Page); }); } private void AddPage(string name, Page page) { TabItem tabItem = MainTabPanel.Items.OfType<TabItem>().FirstOrDefault(item => item.Header.ToString() == name); if (tabItem == null) { tabItem = new TabItem() { Header = name, }; var pageFrame = new Frame(); pageFrame.Focusable = false; pageFrame.BorderThickness = new Thickness(0); pageFrame.Margin = new Thickness(20); pageFrame.Navigate(page); tabItem.Content = pageFrame; MainTabPanel.Items.Add(tabItem); } MainTabPanel.SelectedItem = tabItem; } } public static class ControlHelper { public static T FindVisualParent<T>(DependencyObject sender) where T : DependencyObject { do { sender = VisualTreeHelper.GetParent(sender); } while (sender != null && !(sender is T)); return sender as T; } } /// <summary> /// 主菜单样式选择器 /// </summary> public class MenuStyleSelector : StyleSelector { public override Style SelectStyle(object item, DependencyObject container) { MenuEntity functionItem = item as MenuEntity; if (functionItem.IsGroup) return ControlHelper.FindVisualParent<Menu>(container).Resources["MainMenuStyle"] as Style; else return null; } } }
写在最后
到这里基本上是介绍完了入口项目我认为需要交待的事情,如果有什么不明白的,可以在github上拉取源码将它编译跑起来看看效果就明白了。总的来说这里起了一个协调作用,将辅助功能给统一起来,不需要各子系统或者自己单独再去另行实现。
下一篇文章将介绍JHRS开发框架之各子系统是如何整合的。
本系列相关阅读
- WPF企业级开发框架搭建指南(启示录)
- JHRS开发框架之基础类库
- JHRS开发框架之第三方框架选型
- JHRS开发框架之WPF调用Web API封装
- JHRS开发框架之客户端入口项目
- JHRS开发框架之各子系统如何整合
- JHRS开发框架之怎样设计合理的ViewModel基类
- JHRS开发框架之公用组件用户控件的封装
- JHRS开发框架之建议遵循的一些建目录文件原则
- JHRS开发框架之WPF数据验证
- JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法
- JHRS开发框架之踩坑记(终章)
【江湖人士】(jhrs.com) 投稿作者:IT菜鸟,不代表江湖人士立场,如若转载,请注明出处:https://jhrs.com/2020/38040.html
扫码加入电报群,让你获得国外网赚一手信息。
这个入口项目是个demo项目还是让真正的项目依赖的项目呢
完整的demo项目,因为使用 .net 5开发的,如果你环境没有配置正确,可能存在编译不通过的问题,把 .net 5安装上就可以直接运行了。