站点图标 江湖人士

JHRS开发框架之客户端入口项目5分钟介绍

上一篇文章中,介绍了关于web api的封装事情,其实在做类似这种项目,也可以叫前后端分离的项目吧(客户端用WPF,服务器端是 web api),调用web api这块功能如果自己手工撸的话,确实麻烦,web api的URL(或者路由)开发阶段后端随时可能更改地址或增删参数等,封装后就基本上解决了体力活了,那接下来看看入口项目。

JHRS开发框架之客户端入口项目

如果你在github拉取了源码,可以看到JHRS.Shell这个库就是WPF 客户端的入口项目,是整个系统的一个外壳程序,它会在启动后将各个子系统加载进来;采用这种分而治之的方式将一个大系统划分为多个子系统后,必然有一个入口程序来统一调度和加载各个子系统,凡是需要在各子系统外部实现的功能,都可以放到入口程序来处理,如自动升级功能。

WPF客户端入口项目

JHRS开发框架之客户端入口项目

在上图中可以看到,JHRS框架中的目录并不多,而你要明白,这只是演示项目,真实项目中的要复杂得多,但这个框架基本功能是已经可以满足开发了,需要的功能,自己扩展就可以了;只要明白了为什么这样做,相信猿猿们可以搞定的。

在Shell库中,主要是做了这些事情

  1. App.config保存整个系统的配置信息
  2. App.xaml可以加载整个系统外部资源
  3. ShellSwitcher封装了关闭,打开窗口功能
  4. Views根目录定义了主窗体,主窗体里面布局好各个区域
  5. Login里面放的是登录相关的窗体和页面,用于完成登录进入系统
  6. Dialogs定义了整个系统里面统一使用的模态窗口,消息提示框的展示(调用实现封装到ViewModel基类里面的,因为ViewModel里面做了业务需要各种提示)
  7. ViewModels里面则是入口程序各个页面(Page)或窗体(Window)的后台业务逻辑
  8. 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(数据转换器),各子系统也可以使用。

统一消息提示框和模态窗口

JHRS开发框架之客户端入口项目

在入口项目中,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选项卡展示内容,这种风格的布局也是传统的增删改查项目所爱用的。

JHRS开发框架之客户端入口项目

主窗口的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开发框架之各子系统是如何整合的。

本系列相关阅读

  1. WPF企业级开发框架搭建指南(启示录)
  2. JHRS开发框架之基础类库
  3. JHRS开发框架之第三方框架选型
  4. JHRS开发框架之WPF调用Web API封装
  5. JHRS开发框架之客户端入口项目
  6. JHRS开发框架之各子系统如何整合
  7. JHRS开发框架之怎样设计合理的ViewModel基类
  8. JHRS开发框架之公用组件用户控件的封装
  9. JHRS开发框架之建议遵循的一些建目录文件原则
  10. JHRS开发框架之WPF数据验证
  11. JHRS开发框架之ViewModel相互传参和弹框回传参的解决办法
  12. JHRS开发框架之踩坑记(终章)
退出移动版