WPF(Windows Presentation Foundation)是微软推出的基于Windows 的用户界面框架。

在这里我设计了一份以MVVM设计模式下的纯桌面端应用架构,期间包含界面初始化流程,菜单加载及页面跳转流程等。

以下来详细说明下设计方式:

期间项目使用到了我自己上传到Nuget的包:

WPF 客户端设计(MVVM设计模式)_MVVM

目录

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,自行新增方法;

WPF 客户端设计(MVVM设计模式)_C#_02

当基础数据初始化的工作都做完之后,则调用 静态类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 = ;
                                    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 = ;
                            }
                        }
                    }
                    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用于页面切换

WPF 客户端设计(MVVM设计模式)_MVVM_03

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;

WPF 客户端设计(MVVM设计模式)_MVVM_04

使用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 -- 全局静态类

WPF 客户端设计(MVVM设计模式)_C#_05