• 如何在 MVVM 中优雅地关闭我们的窗口


    问题描述

    最近在进行业务扩展时,我发现我之前封装的 DialogServie 问题越来越多,整个设计思路一点也不 SOLID。这里我简单描述一下:

    DialogServie 采用单例模式。内部定义了一个列表,用于存放当前系统所有打开的窗口实例,然后上层通过调用 Show 方法来创建并显示一个窗口,调用 Close 方法关闭创建,这两个关键函数都有一个重要参数,就是待操作窗口句柄对应的标识,只要标识传递对了,就能对相应的窗口就行操作。那么问题来了,这样岂不是任何地方只要能获取这个标识,就能操作这个窗口了,那到时候窗口一多不就乱套了。这对于整个系统的安全性来说不好,所以我有必要把它进行重构。

    我希望达到的最终效果是,通过抽象工厂,谁想要创建窗口,谁就通过这个工厂来创建,因为每个窗口都会对应着一个 ViewModel,所以谁要是想关闭当前窗口,那就要当前窗口对应的 ViewMoel 具备关闭自己的功能。这样就保证了谁挖的坑到时候就自己负责填,每个坑位互不影响,谁也不要想在乱占坑位。

    问题解决

    基于 MVVM 的 WPF 客户端,在 ViewModel 执行窗口的弹出和关闭是很正常的需求,对于如何优雅地解决这个需求确实需要值得思考一下。这里,我罗列了两种我遇到的场景来进行分享。

    场景一

    假如我们要创建的窗口外观样式不一样,不同窗口都是不同的 Window 。那么这种情况要相对好解决一些,因为我们可以直接将我们要创建的窗口类型传递给窗口工厂,然后将创建的实例句柄返回给上层的消费者。

    我们知道,对于通过调用 ShowDiaog() 的方式显示的窗口,我们可以通过设置其 DialogResultFalse 就能将其窗口关闭,但是由于这个属性不是依赖属性,不能直接使用数据绑定,所以我们需要通过附加属性的方式来解决这个问题。示例代码如下所示:

    public class ContentDialogExtension
    {
        public static bool? GetDialogResult(DependencyObject obj)
        {
            return (bool?)obj.GetValue(DialogResultProperty);
        }
    
        public static void SetDialogResult(DependencyObject obj, bool? value)
        {
            obj.SetValue(DialogResultProperty, value);
        }
    
        // Using a DependencyProperty as the backing store for DialogResult.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DialogResultProperty =
            DependencyProperty.RegisterAttached("DialogResult", typeof(bool?), typeof(ContentDialogExtension), new PropertyMetadata(null, (d, e) =>
            {
                if (d is Window self)
                {
                    self.DialogResult = e.NewValue as bool?;
                }
            }));
    }
    

    然后,我们需要在对应的 ViewModel 中定义相同类型的 属性用于前台数据绑定,我这里的示例代码如下所示:

    public class OtherViewModel : BindableBase
    {
        private bool? _dialogResult;
        public bool? DialogResult
        {
            get { return _dialogResult; }
            set { SetProperty(ref _dialogResult, value); }
        }
    
        private ICommand _closeCommand;
        public ICommand CloseCommand
        {
            get
            {
                if (_closeCommand == null)
                {
                    _closeCommand = new DelegateCommand(() =>
                    {
                        this.DialogResult = true;
                    });
                }
                return _closeCommand;
            }
        }
    }
    

    最后,在 XAML 中进行数据绑定即可,示例代码如下所示:

    <Window
        x:Class="BlankCoreApp1.Controls.DefaultDialog"
        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:local="clr-namespace:BlankCoreApp1.Controls"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="DefaultDialog"
        Width="800"
        Height="450"
        local:ContentDialogExtension.DialogResult="{Binding DialogResult}"
        mc:Ignorable="d">
        <Grid>
            <Button
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Command="{Binding CloseCommand}"
                Content="Close Me" />
        </Grid>
    </Window>
    

    注:由于我这里定义的属性名称和系统的属性名称是一样的,所以有可能会报错,我们可以将其修改为 Code Behind 的方式,示例代码如下所示:

    var bind = new Binding()
    {
        Path = new PropertyPath(nameof(viewModel.DialogResult)),
        Source = viewModel
    };
    SetBinding(ContentDialogExtension.DialogResultProperty, bind);
    

    对于 DialogService 的代码就相对简单,我这里给出的示例代码如下所示:

    public class DialogService
    {
        public static T CreateDialog<T>(object viewmodel) where T : Window, new()
        {
            var dlg = Activator.CreateInstance<T>();
            dlg.DataContext = viewmodel;
            return dlg;
        }
    }
    

    最后,上层调用就直接通过如下方式调用即可:

    var dlg = DialogService.CreateDialog<DefaultDialog>(new OtherViewModel());
    dlg.ShowDialog();
    

    场景二

    假如我们要创建的窗口外观一样,就是内容不一样,我们希望通过 DataTemplate 的方式来动态创建窗口,然后在其内部也要支持关闭当前窗口。

    对于这种需求,我们首先应该想到的是需要提取一个允许在 ViewModel 中关闭对应窗口的接口,然后让其继承该接口。这里,我定义了如下的示例接口:

    public interface IClosable
    {
        bool? DialogResult { get; }
    }
    

    那其对应的 ViewModel 就应该继承并实现该接口,示例代码如下所示:

    public class OtherViewModel : BindableBase, IClosable
    {
        private bool? _dialogResult;
        public bool? DialogResult
        {
            get { return _dialogResult; }
            set { SetProperty(ref _dialogResult, value); }
        }
    
        private ICommand _closeCommand;
        public ICommand CloseCommand
        {
            get
            {
                if (_closeCommand == null)
                {
                    _closeCommand = new DelegateCommand(() =>
                    {
                        this.DialogResult = true;
                    });
                }
                return _closeCommand;
            }
        }
    }
    

    接着,我们就需要创建一个窗口模板,让其能显示传递进来的 DataTemplate,示例代码如下所示:

    <Window.Resources>
        <DataTemplate x:Key="dt">
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
                <Button Command="{Binding CloseCommand}" Content="Close Me" />
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    

    接着,我们定义窗口模板,其对应的前后台示例代码如下所示:

    <tx:ExWindow
        x:Class="Tx.Themes.Dialogs.ContentDialog"
        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:local="clr-namespace:Tx.Themes.Dialogs"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:tx="clr-namespace:Tx.Themes.Controls"
        HorizontalContentAlignment="Stretch"
        VerticalContentAlignment="Stretch"
        Foreground="White"
        ResizeMode="NoResize"
        ShowInTaskbar="False"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterOwner"
        WindowStyle="SingleBorderWindow"
        mc:Ignorable="d" />
    
    public partial class ContentDialog : ExWindow
    {
        public ContentDialog(IClosable viewModel, DataTemplate dataTemplate)
        {
            var bind = new Binding()
            {
                Path = new PropertyPath(nameof(viewModel.DialogResult)),
                Source = viewModel
            };
            SetBinding(ContentDialogExtension.DialogResultProperty, bind);
    
            Init(viewModel, dataTemplate, enableResize);
        }
    
        private void Init(object viewModel, DataTemplate dataTemplate)
        {
            InitializeComponent();
            Owner = Application.Current.Windows.OfType<Window>().FirstOrDefault(x => x.IsActive);
    
            Content = viewModel;
            ContentTemplate = dataTemplate;
        }
    }
    

    然后,我们就需要定义窗口工厂,示例代码如下所示:

    public static class DialogFactory
    {
        public static ContentDialog CreateDialogWithClosable<T>(T ViewModel, DataTemplate dataTemplate, bool enableResize = false) 
            where T : IClosable => new ContentDialog(ViewModel, dataTemplate, enableResize);
    }
    

    最后,上层调用就直接通过如下方式调用即可:

    var dlg = DialogFactory.CreateDialogWithClosable(new NewProjectViewModel(), dt);
    dlg.Title = "新建项目";
    dlg.ShowDialog();
    

    总结

    当然了,我上面说的这几种实现方式你或许觉得麻烦,对软件结构设计不关心,那我这里可以给你介绍一种终极解决方案:消息机制,这种方式可以让你的代码可以肆意游走于任何地方,基本不会受到限制,具体怎么使用可以参考 MVVMLight 里面的 Message,这里就不展开说了,感兴趣的朋友可以自己在 Github 上看看官方源码。

    自由是相对了,那些看似自由的天空或许就是无尽的地狱之渊。

    相关参考

  • 相关阅读:
    get(0)??
    抽象类中。。
    matlab函数
    unity_快捷键
    unity_ UI
    关于博客园使用
    survival shooter
    第七次团队作业:Alpha冲刺(3/10)
    第七次团队作业:Alpha冲刺(2/10)
    第七次团队作业:Alpha冲刺(1/10)
  • 原文地址:https://www.cnblogs.com/hippieZhou/p/9898258.html
Copyright © 2020-2023  润新知