WPF(Windows Presentation Foundation)是微软推出的基于Windows 的用户界面框架。
在这里我设计了一份以MVVM设计模式下的纯桌面端应用架构,期间包含界面初始化流程,菜单加载及页面跳转流程等。
以下来详细说明下设计方式:
期间项目使用到了我自己上传到Nuget的包:
目录
1:启动
2:主界面
2.1 页面跳转
2.2 对话框弹窗
3.BaseViewModel VM基类
3.1 Command使用
4.菜单首页
5.解决方案
1:启动
在App.Xaml.cs 文件中,OnStartup方法,为避免启动两个相同的桌面程序,启动时判断是否已经有相同的程序启动了。使用System.Threading.Mutex (互斥锁)进行判断。
当前进程有效启动时,则开始加载配置文件,一些设置参数是支持文件保存,启动时进行加载。文件内容使用Json字符串。
封装了一层Config的读写类,可以对配置文件进行读写。
var res = ConfigParamOperation.ReadConfigParam(out P_Environment p);
返回的P_Environment,则为配置文件反序列后的对象。
日志的初始化:logPath为日志文件的根目录
LogOperate.InitLog(curProcressID, logPath);
日志提供的了不同的方法,如 Start、Info、Error、Web等等;调用不同的方法,会记录到不同的文件中,就按实际的业务进行处理;
如果提供的LogOperate不满足使用,则可以自定义类继承LogOperateBase,自行新增方法;
当基础数据初始化的工作都做完之后,则调用 静态类Operation.ThreadOperate.OnStart(),开始进行业务流程的初始化,所以业务初始化相关的代码可以写在OnStart方法里。相对应的,程序关闭时,业务结束的流程写在OnExit方法里(都在静态类Operation.hreadOperate里边)。
在App.Xaml.cs文件中,OnLoadCompleted里边增加了两个异常捕获的事件方法,用于捕获UI线程和非UI线程的异常;用于提升客户端的稳定性,当然,最好是在业务方法里边使用Try Catch去捕获异常,因为当最外层连续捕获多次异常后,还是有概率导致进程直接崩溃。
Current.DispatcherUnhandledException += App_OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
以下是OnStartup的方法代码展示:
private static string W_ID = "350088E0-800B-420F-B401-F8A68004E60A";
private static System.Threading.Mutex mutex = null;
protected override void OnStartup(StartupEventArgs e)
{
try
{
curProcressID = Process.GetCurrentProcess().Id;
curProcressName = Process.GetCurrentProcess().ProcessName;
//初始化日志组件
LogOperate.InitLog(curProcressID, logPath);
LogOperate.Start("--------------------------------------------------------------------");
LogOperate.Start("启动.." + curProcressID);
bool flag = false;
mutex = new System.Threading.Mutex(true, W_ID, out flag);
if (!flag)
{
LogOperate.Start("找到互斥线程。");
//等待2秒
System.Threading.Thread.Sleep(2000);
int isContinue = -1;
//寻找客户端进程
Process[] app = Process.GetProcessesByName(curProcressName);
LogOperate.Start("查询已启动线程:" + curProcressName + "..数量:" + app.Length.ToString());
if (app.Length > 0)
{
//判断是否有窗口打开,如果有进程却无窗口显示,则关闭进程后,继续启动当前程序
foreach (var a in app)
{
if (a.MainWindowHandle == IntPtr.Zero)
{
int killId = -1;
//获取线程ID
try
{
killId = a.Id;
if (killId == curProcressID)//当前线程是自己,跳过
continue;
LogOperate.Start(string.Format("获取线程[{0}]...启动时间:{1:HH:mm:ss.ffff}", killId, a.StartTime));
}
catch (Exception ex)
{
LogOperate.Start(string.Format("获取线程ID...失败:{0}", ex.Message));
}
//杀掉线程
try
{
if (killId > 0)
{
LogOperate.Start(string.Format("杀掉线程[{0}]...", killId));
try { a.CloseMainWindow(); a.Close(); System.Threading.Thread.Sleep(1000); } catch { }
a.Kill();
}
}
catch (Exception ex) { LogOperate.Start(string.Format("杀掉线程[{0}]失败:{1}", killId, ex.Message)); }
}
else
{
isContinue = a.Id;
}
}
}
if (isContinue > 0)
{
LogOperate.Start(string.Format("当前进程有效[{0}],退出本次启动。", isContinue));
LogOperate.Save();
Environment.Exit(0);//退出程序
return;
}
}
//主窗体实例化及窗口显示之前,不能进行弹窗
//主窗体实例化
while (GetDesktopWindow() == IntPtr.Zero)
{
LogOperate.Start("未找到桌面句柄,重试...");
System.Threading.Thread.Sleep(100);
}
System.Threading.Thread.Sleep(100);
Task.Run(() =>
{
var res = ConfigParamOperation.ReadConfigParam(out P_Environment p);
if (res)
{
GlobalData.ConfigParams = p;
}
else
{
GlobalData.ConfigParams = new P_Environment();
}
try
{
if (!string.IsNullOrEmpty(GlobalData.ConfigParams.ThemeColor))
{
Application.Current.Resources["ThemeColor"] = (Brush)(new BrushConverter().ConvertFromString(GlobalData.ConfigParams.ThemeColor));
}
}
catch { }
while (true)
{
if (ViewModels.VM_MainWindow.Instance != null)
{
new System.Threading.Thread(() => Operation.ThreadOperate.OnStart()) { IsBackground = true }.Start();
break;
}
Thread.Sleep(500);
}
});
}
catch (Exception ex)
{
LogOperate.Start("OnStartup_Exception Exit " + ex.ToString());
LogOperate.Save();
Environment.Exit(0);
}
}
2:主界面
主界面设计比较简单,就是用一个顶层标题栏,加一个Frame;
标题栏用于显示logo,及窗口控制按钮;Frame用于页面切换
MainWindow 对应的VM 时类VM_MainWindow,继承于BaseViewModel (下面点会详细介绍下VM的基类)
添加了窗口基本的方法,Load,Close等等;
同时也定义了一个静态对象,用于全局可访问VM_MainWindow,用于访问界面跳转,弹窗等等的操作。
public static VM_MainWindow Instance { get; private set; }
2.1 页面跳转
VM_MainWindow.Instance.PageJump(typeof(VM_MenuHomePage));
提供了两个PageJump的方法,按需调用;两个方法区别是什么,入参不同;
一个是传VM的Type类型,然后会在PageJump方法里才创建实例,并显示
一个是传VM的对象实例,在PageJump里边直接使用实例进行页面的切换
/// <summary>
/// 页面跳转
/// </summary>
/// <param name="vmtype">ViewModel 的类型 typeof()</param>
/// <param name="args">ViewModel 的构造函数入参</param>
internal void PageJump(Type vmtype, object[] args = null)
/// <summary>
/// 页面跳转,加载缓存中的vm数据
/// </summary>
/// <param name="vmobject">vm对象</param>
internal void PageJump(object vmobject)
在页面切换时,会调用BeforeJumpCheck 做切换前的判断;如果当前不满足切换的条件,则会组织页面的切换。
2.2 对话框弹窗
窗口需要谈对话框时,调用 VM_MainWindow.Instance.Popup 和VM_MainWindow.Instance.Popup2 方法;
Popup 是单按钮对话框,一直返回true
Popup2 是 双按钮对话框,返回true/false;
/// <summary>
/// 弹出单按钮对话框 永远返回true
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
/// <returns></returns>
public static bool Popup(string title = "提示", string message = "", string comfirm = "确认")
/// <summary>
/// 弹出双按钮对话框
/// </summary>
/// <param name="title"></param>
/// <param name="message"></param>
/// <returns></returns>
public static bool Popup2(string title = "提示", string message = "", string comfirm = "确认", string cancel = "取消")
BackHome,决定返回首页会触发方法,也就是首页的界面显示
/// <summary>
/// 返回首页
/// </summary>
public void BackHome()
{
VM_MainWindow.Instance.PageJump(typeof(VM_MenuHomePage));
}
3.BaseViewModel VM基类
此类的代码在Nuget下载的LS.WPF.MVVM的包中
VM类都继承BaseViewModel; 构造函数定义方法有两种:
第一种:空构造函数,但在base里传入绑定的UI界面的Type
例如: public VM_InitPage() : base(typeof(InitPage))
第二种(一般只用于主界面初始化):入参为UI界面的对象实例
例如: public VM_MainWindow(MainWindow win) : base(win)
继承了BaseViewModel,会在VM初始化时,同时初始化UI对象(UIElement),所以在VM中需要访问UI界面属性时,可以使用UIElement进行访问。
PageLoad及PageUnLoad 的方法已在基类中绑定,重写想要的方法即可。
同时还提供了UI线程运行方法和非UI线程运行方法,当有些代码必须要使用UI线程进行操作时,可以使用 DoMenthodByDispatcher 进行操作,isAsync标识为异步执行和同步执行,默认为异步执行。
public void DoMenthodByDispatcher<T>(Action<T> action, T obj, bool isAsync = true)
还包含了Bitmap转ImageSource的方法,方便用于图片控件的属性绑定
internal static ImageSource GetImageSource(Bitmap b)
3.1 Command使用
当需要绑定Command方法时,架构提供了一个封装类 DelegateCommand
使用方法如下:
/// <summary>
/// 操作方法
/// </summary>
public DelegateCommand OperationCommand
{
get { return new DelegateCommand(Operation); }
}
private void Operation(Object obj)
{
}
Xaml中就直接使用 Command="{Binding OperationCommand}" 即可
4.菜单首页
VM_MainWindow 中,提供Frame用于切换界面;可自由定义。
此处提供了一个左侧菜单栏的首页设计;
MenuHomePage;
使用LSMenu 自定义菜单控件;
<uc:LSMenu.MenuSource>
<uc:LSMenuItem
Header="首页"
ImagePath="/Asset/Images/MenuImg/Home.png"
ItemTag="Home" />
<uc:LSMenuItem
Header="设置"
ImagePath="/Asset/Images/MenuImg/Setting.png"
ItemTag="Setting" />
<uc:LSMenuItem
Header="退出"
ImagePath="/Asset/Images/MenuImg/Exit.png"
ItemTag="Exit" />
</uc:LSMenu.MenuSource>
再对定义的菜单子项做相应的处理。
private void MenuClick(object obj)
{
if(obj!=null)
{
LogOperate.ClickLog("点击了-菜单【" + obj.ToString() + "】");
switch (obj.ToString())
{
case "Home":
VM_MainWindow.Instance.BackHome();
break;
case "Setting":
break;
case "Exit":
VM_MainWindow.Instance.Close();
break;
}
}
}
5.解决方案
整体结构简单明了;
Asset---图片等资源存放
Models -- 数据模型定义区
Operation -- 业务操作方法定义
Tools -- 工具帮助类
UCControls -- 自定义控件区
ViewModels -- VM类存放区
Views -- Xaml UI界面存放区
GlobalData -- 全局静态类