WPFDialogs






4.85/5 (20投票s)
如何继承自定义窗口并使用 WPF 和 MVVM 创建一个返回值的对话框。
目录
窗口:继承自自定义窗口
当您向WPF项目添加Window、Page或UserControl时,将使用默认的继承。您将获得一个继承自WPF的Window
类的窗口。
由于这种默认继承,没有简单的方法可以添加所有新窗口都可以使用的方法、属性或事件。
本教程将引导您完成几个简单的步骤来创建窗口的子类。目标是创建一个窗口基类,我们可以从中继承所有其他窗口。
首先,创建一个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
(整数)。
概述
使用WPF和MVVM设计的对话框包含多个部分,如图2所示。
图2中显示的组件数量可能有点压倒性,但请考虑,每个主要组件都基于一个基类。这是为了促进良好的设计,并且将来更容易添加额外的对话框。
首先,让我们在项目中创建一些文件夹来存放所有部分。在文件夹到位后,我们将开始向它们添加组件。
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
。它可以是int
、string
,或者在本例中,是我们刚刚创建的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
,并设置WindowStyle
和ResizeMode
属性,以便我们的对话框看起来和行为都像一个真正的模态对话框。
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#代码中还有另外两点需要注意。首先,我们将window
的DataContext
设置为ViewModel
的新实例。
DataContext = new MyDialogViewModel();
然后,我们将依赖属性RetValDP
绑定到ViewModel
的Options
属性,该属性的类型为Options
。
SetBinding(RetValDP, "Options");
RetValDP
是我们定义在_DialogBase
类中的DependencyProperty
。Options
是我们将在ViewMode
中定义的属性。
此时需要注意的是,绑定是在设置DataContext
之后在此C#中完成的,因为在XAML的InitializeComponent
期间,RetValDP
和Option
属性不可用。这些在设置DataContext
之后对表单可用。
ViewModel
在MVVM模式中,ViewModel
是我们放置C#代码的类,它成为对话框的数据上下文。这个类将有一个类型为DialogOptions
的属性,名为Options
,以及用于Title
和Age
数据项的属性。
同样,我们将首先创建一个基类:
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.aspx。ViewModelBase
类为我们提供了两个功能,使我们的工作更容易——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
有了保存Title
和Age
的属性,以及返回对象DialogOptions
,让我们来设计视图。请注意,对RaisePropertyChanged
的调用允许View
在属性发生更改时收到通知。这意味着,如果您在ViewModel
中更改了Title
属性,并且它绑定到View
中的UI元素,那么UI元素将反映该更改。要编写自己的通知,您可以改为执行此操作。
View
视图被定义为WPF UserControl
,是我们对话框的UI组件。就像上面创建窗口的子类一样,使用此技术在Views文件夹中创建_ViewBase
和MyDialogView
。完成类的创建后,您应该在Views文件夹中拥有这个基类:
using System;
using System.Windows.Controls;
namespace MVVMDialogWithReturnProperty
{
public class _ViewBase : UserControl
{
}
}
请注意,此类继承自UserControl
。它不包含任何方法或属性。它只是所有视图的一个基类。您可以设计没有继承自基usercontrol
的视图,但此类允许我们在设计中保持一致,并为将来需要扩展所有视图提供一个地方。
在完成_ViewBase
和MyDialogView
类之后,将此添加到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中有两点值得注意:字段绑定到Title
和Age
,以及按钮对命令的引用。首先,我们将讨论绑定,稍后将讨论命令。
由于对话框的数据上下文将是上面创建的MyDialogViewModel
,因此XAML可以绑定到它上面的匹配属性。
一旦设置了DataContext
并设置了绑定,在UI中的Title
或Age
元素所做的更改就会反映在ViewModel
属性中,而ViewModel
中的Title
或Age
属性的更改也会反映在UI元素中。
此时,我们只有一个绑定到ViewModel
的UserControl
。我们需要将该控件放在对话框上。打开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
接口暴露了两个方法:SaveExecuted
和SaveCanExecute
。SaveCanExecute
方法返回一个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元素绑定的Title
和Age
属性值存储到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
时,绑定到它的按钮将被禁用。
消息传递
现在我们的按钮已连接,让我们看一下SaveExecuted
和CancelExecuted
方法中的Message引用。现在我们需要一种方法让我们的viewmodel
告诉对话框该关闭对话框了。我们可以使用GalaSoft消息类来处理这个问题。需要两个步骤才能使其工作:
- 进行消息调用
- 在某处接收消息
由于我们已经在SaveExecuted
和CancelExecuted
方法中进行了消息调用,因此让我们向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();
}
}
}
}
在此部分,我们添加了两项:
- 我们在设置数据上下文后添加了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日:初始版本