65.9K
CodeProject 正在变化。 阅读更多。
Home

WPFDialogs

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (20投票s)

2010年11月24日

CPOL

12分钟阅读

viewsIcon

83920

downloadIcon

1401

如何继承自定义窗口并使用 WPF 和 MVVM 创建一个返回值的对话框。

目录

窗口:继承自自定义窗口

当您向WPF项目添加Window、Page或UserControl时,将使用默认的继承。您将获得一个继承自WPF的Window类的窗口。

图 1. 默认WPF继承

由于这种默认继承,没有简单的方法可以添加所有新窗口都可以使用的方法、属性或事件。

本教程将引导您完成几个简单的步骤来创建窗口的子类。目标是创建一个窗口基类,我们可以从中继承所有其他窗口。

图 2. MainWindow继承自_WindowBase

首先,创建一个WPF应用程序,命名为SubclassWindow。添加一个abstract类,命名为_WindowBase。这个类将作为所有其他窗口的基础。

using System;
using System.Windows;

namespace SubclassWindow
{
    public abstract class _WindowBase : Window
    {
    }
}

接下来,打开MainWindow的XAML并按如下方式修改:

<src:_WindowBase x:Class="SubclassWindow. MainWindow"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:src="clr-namespace:SubclassWindow"
                 Title="WindowsApplication1" Height="300" Width="300"  >

    <Grid>
    </Grid>

</src:_WindowBase>

在上面的XAML中,第一个更改是添加一个新的命名空间标签,以定义基类的位置。

xmlns:src="clr-namespace:SubclassWindow"

接下来,我们按如下方式更改<Window>标签:

<src:_WindowBase x:Class="SubclassWindow. MainWindow"
.
.
.
</src:_WindowBase>

请注意,此更改在XAML中指示此窗口基于_WindowBase,而不是Window

最后,在MainWindow的代码隐藏中,按如下方式更改代码:

public partial class MainWindow : _WindowBase
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

这里唯一的更改是_WindowBase

请注意,如果_WindowBase驻留在另一个应用程序中,您将需要在src标签中添加assembly关键字。

xmlns:src="clr-namespace:SubclassWindow;assembly=the_assembly"

最后,要创建用户控件或页面的子类,请务必从XAML中删除Title标签。

就是这样。现在您可以从_WindowBase继承任何窗口,并访问它的所有成员。

创建一个返回值的对话框,使用WPF和MVVM

目的

本例的目标是使用模型-视图-视图模型(MVVM)模式创建的WPF对话框返回多个用户选择。本教程假设您对MVVM有基本了解。有关MVVM的更多信息,请参阅

本教程还假设您已阅读窗口:继承自自定义窗口

在本例中,我们将创建一个对话框,该对话框将返回一个Title(字符串)和一个Age(整数)。

图 1. 选项对话框

概述

使用WPF和MVVM设计的对话框包含多个部分,如图2所示。

图 2. 概述

图2中显示的组件数量可能有点压倒性,但请考虑,每个主要组件都基于一个基类。这是为了促进良好的设计,并且将来更容易添加额外的对话框。

首先,让我们在项目中创建一些文件夹来存放所有部分。在文件夹到位后,我们将开始向它们添加组件。

图 3. 带有文件夹的解决方案资源管理器

DialogOptions类

为了从对话框返回多个值,我们需要创建一个可以保存所有数据的类。在Classes文件夹中创建以下类。请注意,我已从命名空间中删除了Classes文件夹名称。这是一个偏好的问题,但我发现使用项目组件更容易,如果不需要指定文件夹名称。

namespace MVVMDialogWithReturnProperty
{
    public class DialogOptions
    {
        public string Title { get; set; }
        public int Age { get; set; }
    }
}

对话框

由于对话框基类对我们的设计至关重要,因此我们将逐步介绍它。首先,在Windows文件夹中,添加一个名为_DialogBase的类,并将其派生自WPF的window类。

public class _DialogBase : Window
{
}

创建基类后,我们需要使其通用,以便它可以返回任何类型的值。

public class _DialogBase : Window where T : new()
{
}

接下来,添加一个DependencyProperty,它可以绑定到type T的属性。请记住,T将是我们希望从对话框返回的type。它可以是intstring,或者在本例中,是我们刚刚创建的DialogOptions类的实例。让我们将此属性命名为RetValDP,因为它是一个返回依赖属性。

public class _DialogBase<T> : Window where T : new()
{
    public static readonly DependencyProperty RetValDP =
        DependencyProperty.Register("RetVal", typeof(T), 
	typeof(_DialogBase<T>), new FrameworkPropertyMetadata());
}

接下来,添加一个名为RetVal的属性,它将保存一个我们正在返回的type的引用。

public class _DialogBase<T> : Window where T : new()
{
    // Create a dependency property to hold the dialog's return value
    public static readonly DependencyProperty RetValDP =
        DependencyProperty.Register("RetVal", typeof(T), 
		typeof(_DialogBase<T>), new FrameworkPropertyMetadata());

    public T RetVal
    {
        get { return (T)GetValue(RetValDP); }
        set { SetValue(RetValDP, value); }
    }
}

最后,在构造函数中,将RetVal初始化为我们正在返回的任何type,并设置WindowStyleResizeMode属性,以便我们的对话框看起来和行为都像一个真正的模态对话框。

public _DialogBase()
{
    RetVal = new T();

    // Set the window up like a dialog
    WindowStyle = WindowStyle.SingleBorderWindow;
    ResizeMode = ResizeMode.NoResize;
}

稍微跳过一下,我们将在本教程中做的最后一件事就是通过以下方式测试我们的工作:

MyDialog dialog = new MyDialog();
dialog.ShowDialog();

if (dialog.RetVal != null)
{
    string name = dialog.RetVal.Title;
    int age = dialog.RetVal.Age;
}

从这个测试中,您可以看到我们将如何使用RetVal属性。

完成的_DialogBase类代码应如下所示:

public class _DialogBase<T> : Window where T : new()
{
    public static readonly DependencyProperty RetValDP =
        DependencyProperty.Register("RetVal", typeof(T), 
	typeof(_DialogBase<T>), new FrameworkPropertyMetadata());

    public T RetVal
    {
        get { return (T)GetValue(RetValDP); }
        set { SetValue(RetValDP, value); }
    }

    public _DialogBase()
    {
        RetVal = new T();

        WindowStyle = WindowStyle.SingleBorderWindow;
        ResizeMode = ResizeMode.NoResize;
    }
}

创建DialogBase类的子类

现在已经创建了对话框基类,我们需要将其子类化为一个工作的对话框。在Windows文件夹中创建一个名为MyDialog的WPF窗口。

为了使_DialogBase类能够返回我们DialogOptions类的实例,我们需要在上面代码中创建的RetVal属性中指定要返回的类型。为此,请按如下方式修改_DialogBase的XAML和代码隐藏:

<src:_DialogBase x:Class="MVVMDialogWithReturnProperty.MyDialog"
                 x:TypeArguments="src:DialogOptions"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:src="clr-namespace:MVVMDialogWithReturnProperty"
                 xmlns:MyDialogViewNS="clr-namespace:MVVMDialogWithReturnProperty"
                 Title="My Dialog"
                 Height="331"
                 Width="518">


    <Grid>
    </Grid>

</src:_DialogBase>

请注意,我们的窗口基于_DialogBase,并且我们添加了TypeArguments标签(上面粗体显示)。由于我们在MyDialog类中,所以指定我们想要返回DialogOptions是可以的,因为DialogOptions是该特定对话框返回值的类。代码隐藏也需要知道返回类型:

using System;

namespace MVVMDialogWithReturnProperty
{
    public partial class MyDialog : _DialogBase
    {
        public MyDialog()
        {
            InitializeComponent();

            DataContext = new MyDialogViewModel();

            SetBinding(RetValDP, "Options");
        }
    }
}

再次注意,我们在这里指定了DialogOption类型作为基类型。这与XAML中的标签结合使用,告诉对话框返回什么类型。此时,_DialogBase中的RetVal的类型现在是DialogOptions

C#代码中还有另外两点需要注意。首先,我们将windowDataContext设置为ViewModel的新实例。

DataContext = new MyDialogViewModel();

然后,我们将依赖属性RetValDP绑定到ViewModelOptions属性,该属性的类型为Options

SetBinding(RetValDP, "Options");

RetValDP是我们定义在_DialogBase类中的DependencyPropertyOptions是我们将在ViewMode中定义的属性。

此时需要注意的是,绑定是在设置DataContext之后在此C#中完成的,因为在XAML的InitializeComponent期间,RetValDPOption属性不可用。这些在设置DataContext之后对表单可用。

ViewModel

在MVVM模式中,ViewModel是我们放置C#代码的类,它成为对话框的数据上下文。这个类将有一个类型为DialogOptions的属性,名为Options,以及用于TitleAge数据项的属性。

同样,我们将首先创建一个基类:

using System;

namespace MVVMDialogWithReturnProperty
{
    public partial class MyDialog : _DialogBase<DialogOptions>
    {
        public MyDialog()
        {
            InitializeComponent();

            DataContext = new MyDialogViewModel();

            SetBinding(RetValDP, "Options");
        }
    }
}

请注意对GalaSoft MVVMLight工具包的引用。我们的类继承自该工具包中的ViewModelBase类。您可以在此处阅读有关它的信息:http://blog.galasoft.ch/Default.aspxViewModelBase类为我们提供了两个功能,使我们的工作更容易——RaisePropertyChanged事件和命令(Commanding)。通过创建上述基类,我们可以利用GalaSoft工具包,并且仍然有一个扩展ViewModel类的地方。

接下来,我们创建对话框的ViewModel,即MyDialogViewModel。我们将分两步进行。我们现在将进行第一步,稍后在讨论命令时再返回第二步。首先定义我们上面讨论的属性:

using System;

namespace MVVMDialogWithReturnProperty
{
    public class MyDialogViewModel : _ViewModelBase
    {
        private DialogOptions _Options = new DialogOptions();
        public DialogOptions Options
        {
            get { return _Options; }
            set
            {
                _Options = value;
                RaisePropertyChanged("Options");
            }
        }

        private string _Title = string.Empty;
        public string Title
        {
            get { return _Title; }
            set
            {
                _Title = value;
                RaisePropertyChanged("Title");
            }
        }

        private int _Age = 0;
        public int Age
        {
            get { return _Age; }
            set
            {
                _Age = value;
                RaisePropertyChanged("Age");
            }
        }

    }
}

现在我们的viewmodel有了保存TitleAge的属性,以及返回对象DialogOptions,让我们来设计视图。请注意,对RaisePropertyChanged的调用允许View在属性发生更改时收到通知。这意味着,如果您在ViewModel中更改了Title属性,并且它绑定到View中的UI元素,那么UI元素将反映该更改。要编写自己的通知,您可以改为执行操作。

View

视图被定义为WPF UserControl,是我们对话框的UI组件。就像上面创建窗口的子类一样,使用技术在Views文件夹中创建_ViewBaseMyDialogView。完成类的创建后,您应该在Views文件夹中拥有这个基类:

using System;
using System.Windows.Controls;

namespace MVVMDialogWithReturnProperty
{
    public class _ViewBase : UserControl
    {
    }
}

请注意,此类继承自UserControl。它不包含任何方法或属性。它只是所有视图的一个基类。您可以设计没有继承自基usercontrol的视图,但此类允许我们在设计中保持一致,并为将来需要扩展所有视图提供一个地方。

在完成_ViewBaseMyDialogView类之后,将此添加到MyDialogView的XAML中:

<UserControl.Resources>
    <Style TargetType="Button">
        <Setter Property="Width" Value="75"/>
        <Setter Property="Height" Value="30"/>
        <Setter Property="Margin" Value="2"/>
        <Setter Property="FontFamily" Value="Verdana"/>
        <Setter Property="FontSize" Value="11px"/>
    </Style>
</UserControl.Resources>

<Grid Margin="5">

    <DockPanel LastChildFill="True">

        <!--Lower Button Panel-->
        <StackPanel Orientation="Horizontal"
                HorizontalAlignment="Right"
                DockPanel.Dock="Bottom">

            <!--Save Button-->
            <Button Name="cmdSave"
                    Command="{Binding SaveCommand}"
                    Content="Save">
            </Button>

            <!--Cancel Button-->
            <Button Name="cmdCancel"
                    Command="{Binding CancelCommand}"
                    Content="Cancel">
            </Button>

        </StackPanel>

        <Grid>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"></RowDefinition>
                <RowDefinition></RowDefinition>
            </Grid.RowDefinitions>

            <Grid Margin="5,5,5,5" Grid.Row="1">

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"></RowDefinition>
                    <RowDefinition Height="Auto"></RowDefinition>
                    <RowDefinition Height="Auto"></RowDefinition>
                </Grid.RowDefinitions>

                <Label Name="lblTitle"
                        Content="Title:"
                        FontFamily="Arial"
                        Grid.Column="0"
                        Grid.Row="0"
                        HorizontalAlignment="Right"
                        FontSize="14"/>
                <TextBox Name="txtTitle"
                        Margin="5,2,5,2"
                        Text="{Binding Path=Title, Mode=TwoWay}"
                        Grid.Column="2"
                        Grid.Row="0"
                        VerticalAlignment="Top"
                        FontSize="12"/>

                <!-- User Name-->
                <Label Name="lblAge"
                        Content="Age:"
                        FontFamily="Arial"
                        Grid.Column="0"
                        Grid.Row="1"
                        HorizontalAlignment="Right"
                        FontSize="14"/>
                <TextBox Name="txtAge"
                        Margin="5,2,5,2"
                        Text="{Binding Path=Age, Mode=TwoWay}"
                        Grid.Column="1"
                        Grid.Row="1"
                        VerticalAlignment="Top"
                        FontSize="12"/>

            </Grid>

        </Grid>

    </DockPanel>

</Grid>

请注意,本教程的目标不是设计最令人愉悦的UI,所以它非常简单,也不太漂亮。视图的代码隐藏不会有任何更改。在此XAML中有两点值得注意:字段绑定到TitleAge,以及按钮对命令的引用。首先,我们将讨论绑定,稍后将讨论命令。

由于对话框的数据上下文将是上面创建的MyDialogViewModel,因此XAML可以绑定到它上面的匹配属性。

图 4. 绑定到ViewModel属性的XAML

一旦设置了DataContext并设置了绑定,在UI中的TitleAge元素所做的更改就会反映在ViewModel属性中,而ViewModel中的TitleAge属性的更改也会反映在UI元素中。

此时,我们只有一个绑定到ViewModelUserControl。我们需要将该控件放在对话框上。打开MyDialog.xaml文件并按如下方式修改:

<src:_DialogBase x:Class="MVVMDialogWithReturnProperty.MyDialog"
                 x:TypeArguments="src:DialogOptions"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:src="clr-namespace:MVVMDialogWithReturnProperty"
                 xmlns:MyDialogViewNS="clr-namespace:MVVMDialogWithReturnProperty"
                 Title="My Dialog"
                 Height="331"
                 Width="518">


    <Grid>

        <MyDialogViewNS:MyDialogView/>

    </Grid>

</src:_DialogBase>

这里唯一的更改是添加了

    <MyDialogViewNS:MyDialogView/>

这会将View添加到窗口。添加后,您应该会在设计器中看到来自MyDialogView控件的UI组件。

Commands

现在我们的ViewModel可以接收UI值了,我们需要对数据做些处理。当用户单击“保存”按钮时,我们将值写入DialogOptions类。当用户单击“取消”按钮时,我们将关闭对话框,不再进行任何进一步操作。

由于我们使用的是MVVM模式,我们希望尽可能将C#代码放在viewmodel中。由于按钮是在视图的XAML中定义的,而代码在viewmodel中,因此我们需要一种方法来处理按钮点击。如果我们简单地为按钮分配一个事件处理程序,事件定义默认情况下将位于视图的代码隐藏中,而不是视图模型中。

为了使此工作正常进行,我们可以实现ICommand接口。有关ICommand的概述,请参阅http://msdn.microsoft.com/en-us/library/system.windows.input.icommand.aspx。另请参阅http://msdn.microsoft.com/en-us/library/ms752308.aspx

ICommand接口非常简单。它只包含三个成员,我们将在其中使用两个。要处理按钮的单击,我们需要首先添加对System.Windows.Input的引用,并为其添加一个using语句。接下来,我们将进入ViewModel并为每个按钮定义一个命令属性,如下所示:

private ICommand saveCommand = null;
public ICommand SaveCommand
{
    get
    {
        if (saveCommand == null)
            saveCommand = new RelayCommand(SaveExecuted, SaveCanExecute);

        return saveCommand;
    }
}

对“取消”按钮也执行相同的操作。

ICommand接口暴露了两个方法:SaveExecutedSaveCanExecuteSaveCanExecute方法返回一个bool,您可以在其中放置逻辑来确定按钮是否应该启用。SaveExecuted方法是处理鼠标单击的地方。C#使用从SaveCanExecute方法返回的值来自动启用或禁用按钮。因此,您可以同时使用相同的命令来实现菜单栏、按钮和上下文菜单——并且只需在一个位置编写代码来处理命令。连接到命令的任何对象都会根据您在SaveCanExecute方法中编写的逻辑自动启用或禁用。

您会注意到RelayCommand的使用。RelayCommand定义在前面讨论的GalaSoft工具包中,它实现了ICommand接口并对其进行了扩展。您应该已经有一个对其的引用和一个using语句。如果没有,请立即添加。

定义了两个属性后,添加方法定义:

public bool CancelCanExecute()
{
    return true;
}
public void CancelExecuted()
{
    Options = null;
    Messenger.Default.Send(WindowMessages.CloseWindow);
}

public bool SaveCanExecute()
{
    return (Title != string.Empty && Age > 0);
}
public void SaveExecuted()
{
    _Options.Title = _Title;
    _Options.Age = _Age;

    Messenger.Default.Send(WindowMessages.CloseWindow);
}

请记住,每个属性都会调用其关联的CanExecute方法。对于“取消”按钮,我们返回true,因为我们总是希望它启用,这样用户就可以随时退出对话框。但对于“保存”按钮,我们只希望在通过UI填写对话框的属性时才启用它。一旦我们将这两个属性附加到按钮上,UI就会在各个点调用CanExecute方法来确定何时启用按钮。

SaveExecuted将UI元素绑定的TitleAge属性值存储到Options属性中存储的DialogOptions类的实例中。CancelExecuted清除了Options属性。这两个方法都进行Messenger调用,将在下一节中介绍。

要将命令属性附加到UI,请转到MyDialogView类的XAML并添加以下内容:

<!--Save Button-->
<Button Name="cmdSave"
        Command="{Binding SaveCommand}"
        Content="Save">
</Button>

<!--Cancel Button-->
<Button Name="cmdCancel"
        Command="{Binding CancelCommand}"
        Content="Cancel">
</Button>

请注意,我们现在已将代码添加到XAML中,以将按钮绑定到视图模型上的命令属性。我们只需要这样做。从这一点开始,当CanExecute方法返回false时,绑定到它的按钮将被禁用。

消息传递

现在我们的按钮已连接,让我们看一下SaveExecutedCancelExecuted方法中的Message引用。现在我们需要一种方法让我们的viewmodel告诉对话框该关闭对话框了。我们可以使用GalaSoft消息类来处理这个问题。需要两个步骤才能使其工作:

  • 进行消息调用
  • 在某处接收消息

由于我们已经在SaveExecutedCancelExecuted方法中进行了消息调用,因此让我们向MyDialog的代码隐藏添加一些代码来接收调用:

namespace MVVMDialogWithReturnProperty
{
    public partial class MyDialog : _DialogBase<DialogOptions>
    {
        public MyDialog()
        {
            InitializeComponent();

            DataContext = new MyDialogViewModel();

            Messenger.Default.Register<WindowMessages>(this, _RecieveMessage);

            SetBinding(RetValDP, "Options");
        }

        private void _RecieveMessage(WindowMessages Message)
        {
            if (Message == WindowMessages.CloseWindow)
            {
                Close();
            }
        }
    }
}

在此部分,我们添加了两项:

  1. 我们在设置数据上下文后添加了Register调用。这告诉Messenger类,此对话框可以在_ReceiveMessage方法中接收类型为WindowMessages的消息。

请确保在Enums文件夹中按如下方式创建enum WindowMessages

public enum WindowMessages
{
    CloseWindow
}

使用此技术,您可以将任何类型的数据在两个对象之间传递。

最后

让我们运行它看看会发生什么。打开MainWindow的代码隐藏,并在InitializeComponent调用后添加以下内容:

MyDialog dialog = new MyDialog();
dialog.ShowDialog();

if (dialog.RetVal != null)
{
    string name = dialog.RetVal.Title;
    int age = dialog.RetVal.Age;
}

if语句上放置一个断点并运行它。在对话框字段中输入一些数据。保存按钮应该会启用。单击“保存”按钮会将数据存储到Options属性中,并关闭窗口。单步进入if语句,您的数据将在那里。

摘要

诚然,这有很多代码。但这也是我们开始时创建三个基类的原因。从它们创建的未来对话框将需要更少的代码,并且可以更轻松、更快速地在WPF/MVVM中创建返回复杂值的模态对话框。

历史

  • 2010年11月22日:初始版本
© . All rights reserved.