上一篇文章中,介绍了关于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安装上就可以直接运行了。