如何在 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 上看看官方源碼。

自由是相對了,那些看似自由的天空或許就是無盡的地獄之淵。

相關參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM