MVVM 和 WPF DataGrid






4.94/5 (59投票s)
使用 MVVM,特别是与 WPF DataGrid 结合使用

引言
本文最初是作为 WPF DataGrid
演示和一篇短文,解释了如何使用 DataGrid
完成基本任务。在过去的几天里,我一直在深入研究 WPF DataGrid
,我对我的发现感到惊喜。它非常容易使用,并且能产生相当好的结果。我整理了一个演示,记录了我将在即将到来的项目中需要使用 DataGrid
执行的基本操作,我想与 CodeProject 社区分享。特别是,我想分享我在行拖动过程中在网格上绘制拖放线的方法。
在撰写本文时,我发现我处理 DataGrid
的大部分方式都由模型-视图-视图模型模式驱动,因此本文最终既是关于 MVVM,也是关于 DataGrid
。我认为,如果你正在尝试掌握 MVVM,它会特别有帮助,尽管即使是经验丰富的人也会发现 DataGrid
的讨论很有用。
本文随附的演示应用程序将以下基本操作的“操作指南”集中在一起:
- 设置
DataGrid
的样式 - 从
DataGrid
获取和发送数据 - 向
DataGrid
添加项目 - 从
DataGrid
删除项目 - 通过在
DataGrid
内拖放项目来移动它们
正如您在通读本文时将看到的那样,MVVM 极大地影响了使用 DataGrid
的几乎每个方面。对于许多开发人员,包括我自己,它只是 WPF 的实现方式。
运行演示应用程序
对于演示应用程序,我需要一个没有自然顺序的简单模型,所以我借用了上周五的家庭购物清单,并从中制作了一个对象模型。它是一种可用于执行上面列出的所有 DataGrid
任务的列表。我没有在演示中包含数据持久性,因为有很多方法可以做到这一点。相反,我在运行时创建对象模型。
当您启动演示时,您将在 DataGrid
上看到一个购物清单。该网格有两列:
序列
:列表中每个项目的序列号。我包含此列只是为了验证当项目移动时(我将在下面讨论)列表的重新排序是否正在进行。项目
:购物清单项目。
以下是如何执行上面描述的基本 DataGrid
操作:
- 向列表添加项目:单击列表底部的空白行。
DataGrid
将创建一个新的空项目并为其分配下一个序列号。输入项目的名称并按 Enter 键。 - 从列表中删除项目:选择一个项目并单击
DataGrid
上方的“删除”按钮。该项目将被删除,其余项目将重新排序。 - 移动项目:按住 Shift 键并单击一个项目。继续按住 Shift 键,将项目拖到另一行,然后松开。当您拖动项目时,网格上会出现一条拖放线,向您显示如果放下项目,它将被插入到哪里。当您放下项目时,列表将重新排序。
演示就这么多了。正如我最初所说,它相当简单。
模型-视图-视图模型实现
该演示围绕模型-视图-视图模型 (MVVM) 模式构建。我使用了一种相当传统的 MVVM 实现。目前 MVVM 有几种不同的风格,许多关于 MVVM 的文献没有区分我认为至关重要的一点。
两种风格的 MVVM:MVVM 从另外两种模式演变而来,即模型-视图-控制器 (MVC) 和模型-视图-演示器 (MVP)。两种模式都是将业务和控制逻辑从 UI (View) 中移出的方法。在这两种模式中,协调器类都位于 UI 和域模型之间。协调器类与 View 通信,并包含原本会进入 View 代码隐藏的业务和控制逻辑。
然而,这两种模式无论多么相似,它们都不是相同的。我个人在这两种模式之间划出以下区别:
- 在 MVC 中,控制器不应该了解其视图。它只是公开视图可以使用的属性和方法。它对实际消费其服务的视图是漠不关心的。
- 在 MVP 中,演示器通常对其视图有深入的了解;视图实际上只是演示器的一个附属物。在 MVP 应用程序中,视图是演示器的一个属性并不罕见,以便于两者之间的轻松通信。
我个人强烈偏爱 MVVM 的 MVC 风格。我相信 MVC 方法让我有更大的灵活性来更改 View,而不会在 ViewModel 中造成意外后果。换句话说,我相信它能更好地促进关注点分离,并使 View 和 ViewModel 相互隔离,同时允许两者之间轻松通信。我在我的 CodeProject 关于 MVC 的文章中更详细地讨论了该主题,可以在此处找到。
因此,我的 ViewModel 对使用它的 View 一无所知。ViewModel 由 App.xaml.cs 作为应用程序引导过程的一部分传递给 View。
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Initialize main window and view model
var mainWindow = new MainWindow();
var viewModel = new MainWindowViewModel();
mainWindow.DataContext = viewModel;
mainWindow.Show();
}
视图模型被实例化并设置为主窗口的 DataContext,主窗口用作演示应用程序的 View。从那里,XAML 可以使用 WPF 数据绑定访问视图模型的属性。
视图模型的元素
在其他方面,我的 MVVM 实现非常简单。视图模型由以下类组成:
- MainWindowViewModel.cs:这是视图模型的主要类。
- DeleteItemCommand.cs:一个从
DataGrid
删除对象的ICommand
类。 - SequencingService.cs:一个服务类,用于在
DataGrid
上移动项目时重新排序购物清单。 - IgnoreNewItemPlaceHolderConverter.cs:一个值转换器,用于解决 WPF
DataGrid
中的一个讨厌的 bug。更多内容请参见下文。
这种使用主类、命令、服务和值转换器构建视图模型的方式是我的 MVVM 实现的典型做法。
MVVM 模式旨在促进 WPF 数据绑定的使用。我不知道 WPF 实现是否可以被视为数据绑定的 3.0 版本,但微软在此版本中绝对做得非常出色。WPF 数据绑定极大地简化了将视图与其视图模型连接起来的过程。几乎所有事情都作为简单的绑定完成:
<toolkit:DataGrid x:Name="MainGrid"
ItemsSource="{Binding GroceryList}"
SelectedItem="{Binding SelectedItem, ...}" />
即使命令也是通过数据绑定连接的
<Button Command="{Binding DeleteItem}" ... />
连接视图的过程比在 WinForms 中更简单快捷。仅此一点就足以证明转向 WPF 是合理的。
视图模型属性:我个人的编程风格是将项目分组并大量使用区域进行分组。在演示应用程序中,视图模型属性分为两类,这在我的 MVVM 实现中相当典型:
- 命令属性:这些是
ICommand
对象,它们执行视图模型的实际工作。这些属性通常绑定到按钮和类似控件的Command
属性。演示应用程序有一个命令属性DeleteItem
,它从
中删除一个项目。DataGrid
- 数据属性:这些是我们传统上认为的属性。演示应用程序有两个属性;
GroceryItem
和SelectedItem
。
在某些项目中,我将包含第三类 MVVM 属性,即 UtilityProperties
。这些属性保存视图可能需要提供给视图模型的标志和其他值。我在演示应用程序中不需要它们,所以它只有上面列出的两种类型的属性。
命令属性
在我的 MVVM 实现中,ViewModel 很少执行实际工作。相反,它充当协调器对象,并将工作委托给命令和服务对象。Command
对象是 MVVM 模式的标准元素,同样,有几种实现它们的方法。在我的实现中,我喜欢将每个任务的主要逻辑放入命令对象中。因此,我的命令属性都是 ICommand
对象,每个都链接到一个实现 ICommand
接口的唯一类。
ICommand
接口的真正好处在于它提供了一个方便的框架来维护视图中显示的控件的启用/禁用状态。例如,演示应用程序显示一个从网格中删除选定项目的按钮。该按钮绑定到视图模型的 DeleteItem
命令属性:
<Button Command="{Binding DeleteItem}" ... >
该属性链接到一个 ICommand
类 DeleteItemCommand.cs。每个 ICommand
类都必须实现一个 CanExecute()
方法,该方法确定命令在任何给定时间点是否可以执行。以下是 DeleteItemCommand.cs 中该方法的实现:
/// <summary>
/// Whether this command can be executed.
/// </summary>
public bool CanExecute(object parameter)
{
return (m_ViewModel.SelectedItem != null);
}
.NET 会定期轮询所有 ICommand
对象的此方法。如果该方法返回 true
,.NET 会启用绑定到该命令的任何控件。如果该方法返回 false
,则 .NET 会禁用绑定控件。在 DeleteItemCommand.cs 中,CanExecute()
方法只是检查视图模型的 SelectedItem
属性,以查看网格中是否选中了任何内容。如果是,则该方法返回 true
,并且按钮被启用。否则,该方法返回 false
,并且按钮由 .NET 自动禁用。这是支持 WPF 的另一个有力论据。
请注意,DeleteItemCommand.cs 在命令的 m_ViewModel
成员变量中保存对其父视图模型的引用。此引用是在视图模型初始化其命令属性时设置的:
private void Initialize()
{
// Initialize commands
this.DeleteItem = new DeleteItemCommand(this);
...
}
视图模型实例化与其命令属性链接的 ICommand
对象,并将自身引用传递给每个 ICommand
类的构造函数。此引用使命令可以轻松地在执行工作时检查视图模型属性。
ICommand
的实际工作在其 Execute()
方法中执行。在演示应用程序中,工作非常简单。Execute()
方法从视图模型获取当前选定的项目并删除该项目:
public void Execute(object parameter)
{
var selectedItem = m_ViewModel.SelectedItem;
m_ViewModel.GroceryList.Remove(selectedItem);
}
数据属性
数据属性非常简单明了,因为它们类似于我们从一开始就在 .NET 中使用的对象属性。唯一的区别是 WPF 要求视图模型中的数据属性触发 PropertyChanged
事件,以便在视图模型属性更改时可以更新视图。
在演示应用程序中,我们通过两种方式满足此要求。首先,我们将视图模型从一个位于演示应用程序的 UtilityClasses 文件夹中的 abstract
类 ViewModelBase.cs 派生。该类实现了 INotifyPropertyChanged
接口,这是一个 .NET 接口,要求实现类提供 PropertyChanged
事件。ViewModelBase
类还提供了一个方法 RaisePropertyChangedEvent()
,用于在属性更改时触发该事件。
我们的视图模型使用简单的自动属性作为命令属性,如下所示:
public ICommand DeleteItem { get; set; }
但它对数据属性使用更复杂的属性声明,如下所示:
public ObservableCollection<GroceryItem> GroceryList
{
get { return p_GroceryList; }
set
{
p_GroceryList = value;
base.RaisePropertyChangedEvent("GroceryList");
}
}
数据属性声明的形式允许我们在数据属性更改时调用视图模型基类中的 RaisePropertyChangedEvent()
。这就是使视图和视图模型保持同步的原因。
服务类
有些任务是:
- 被多个命令使用
- 足够长,以至于我不想用它们的代码来使命令对象变得杂乱,或者
- 由视图模型调用,而不是视图中的控件
我使用服务类来处理这些代码,我将其视为视图模型的一部分。在我的 MVVM 实现中,服务类通常是 static
的。
NHibernate 的怪癖:在演示应用程序中,我们有一个服务类 SequencingService.cs。我的应用程序通常使用 NHibernate 进行数据持久化。我是 NHibernate 的忠实粉丝;它大大减少了创建代码以从数据库加载对象并将其保存回数据库所花费的时间。但它确实有一些怪癖。
我最喜欢 NHibernate 的一点是它自动进行“脏检查”。它只会保存已更改的对象,这大大加快了数据持久化的速度。但要使此功能正常工作,我们必须非常小心地保留从数据库中读取的对象的标识。例如,如果我们重新排序从数据库加载的集合,NHibernate 会将重新排序的集合视为一个新集合,我们将失去自动脏检查。幸运的是,我们可以向集合添加对象并从中删除对象,而不会丢失脏检查。
但事实证明,NHibernate 对 MVVM 所基于的 ObservableCollection<T>
集合类型也有厌恶。为了解决这个限制,我创建了一个应用程序,它将 NHibernate 喜欢的普通 IList<T>
对象包装在 WPF 喜欢的 ObservableCollection<T>
包装器中。您可以在此处找到该应用程序和文档。
本文附带的演示应用程序不涉及 NHibernate,但我即将推出的生产应用程序(演示应用程序作为其试点项目)确实涉及。在该应用程序中,我将需要为视图显示的集合中的每个对象保存一个序列号,以便在下次读取时可以以正确的顺序加载这些对象。这被证明是一项相当简单的任务。
当我们如上所述使用包装器对象时,当我们在数据网格中拖放项目时,重新排序的是包装器对象,而不是域对象。域对象在域集合中保持其原始顺序。我们所需要做的就是在移动、添加或删除之后重新排序包装器集合。
SequencingService 类和 ISequencedObject 接口:SequencingService
类负责处理这个问题。它有一个方法 SetCollectionSequence()
,它只是遍历列表并根据其当前索引重新编号每个项目:
public static ObservableCollection<T> SetCollectionSequence<T>(ObservableCollection<T>
targetCollection) where T : ISequencedObject
{
// Initialize
var sequenceNumber = 1;
// Resequence
foreach (ISequencedObject sequencedObject in targetCollection)
{
sequencedObject.SequenceNumber = sequenceNumber;
sequenceNumber++;
}
// Set return value
return targetCollection;
}
此方法的签名需要一些解释。在我的生产应用程序中,我将有几个集合将由应用程序的视图显示。这些集合不一定派生自同一个基类,因此我创建了一个接口 ISequencedObject
,以便我可以将其中任何一个集合传递给此服务方法。接口文件位于演示应用程序的 UtilityClasses 文件夹中。它包含一个属性:
public interface ISequencedObject
{
int SequenceNumber { get; set; }
}
因此,SetCollectionSequence()
方法可以处理任何 ObservableCollection<T>
,只要集合的元素实现 ISequencedObject
接口即可。这就是方法签名末尾的 where 子句所表示的。
服务类经常从 ICommand
对象中调用。但是,演示应用程序展示了使用服务类的另一个原因。重新排序集合不是由按钮或其他控件触发的。相反,它是在购物清单更改时调用的。也就是说,当一个项目被添加到列表、从列表中删除或在列表中移动时。在其 Initialize()
方法中,视图模型订阅了购物清单的 CollectionChanged
事件:
p_GroceryList.CollectionChanged += OnGroceryListChanged;
当事件触发时,视图模型中的事件处理程序调用排序服务:
void OnGroceryListChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
...
// Resequence list
SequencingService.SetCollectionSequence(this.GroceryList);
}
我们本可以直接将重新排序代码放在事件处理程序中,但在复杂的生产应用程序中,视图模型会很快变得杂乱。在我即将推出的应用程序中,该代码将由多个不同的视图模型调用。这些要求需要一个可供需要它的任何人访问的服务类。
请记住,排序服务对于 WPF 或 MVVM 而言都不是必需的。它的需求源于我相当独特的需求,这些需求源于将 NHibernate 与 WPF DataGrid
一起使用。尽管如此,即使假设这些需求不适用于您的应用程序,服务类也演示了在 MVVM 应用程序中何处以及如何使用服务的一种方法。这是一种对我来说非常有效的方法,我毫不犹豫地推荐它用于一般用途。
值转换器
值转换器是 MVVM 模式中另一个常用元素。WPF 通常以字符串形式进行通信,我们使用值转换器将对象值转换为其字符串表示。值转换器的经典示例是日期转换器,它可能将一个 DateTime
对象(其默认字符串表示为“9/20/2009 12:00:00 AM”)转换为更用户友好的表示形式,例如“2009 年 9 月 20 日星期日”。
在演示应用程序中,我们没有任何自然发生的值转换需求,但我们确实有一个非常重要的值转换器用途。Pre-C# 4.0 WPF Toolkit 中包含的 WPF DataGrid
版本有一个相当微妙但非常恶劣的错误。如果您将 DataGrid
的 SelectedItem
属性数据绑定到视图模型(如演示应用程序所示),您将无法再使用 DataGrid
通过单击网格底部的空白行向网格添加新项的功能。如果您尝试以这种方式添加新项,您将收到一个 FormatException
,几乎没有解释发生了什么或在哪里发生。
感谢 Nigel Spencer,他记录了这个错误并提供了一个巧妙的修复。问题似乎出在 DataGrid
为新项生成的值上。通过使用一个简单地忽略该返回值的转换器可以避免这个问题。这就是演示应用程序中的值转换器所做的;它是 Nigel 建议的转换器的直接复制。您可以在此处阅读有关此错误和 Nigel 解决方案的更多信息。
请注意,我们必须在代码中声明值转换器——请参见类文件 IgnoreNewItemPlaceHolderConverter.cs。但我们还需要在每个带有 DataGrid
的窗口的 XAML 中引用值转换器。在演示应用程序中,我们在主窗口 XAML 的 <Window.Resources>
部分中执行此操作:
<Window.Resources>
...
<!-- Value Converters -->
<vc:IgnoreNewItemPlaceHolderConverter x:Key="ignoreNewItemPlaceHolderConverter"/>
...
</Window.Resources>
正如您将在下面看到的,更常见的是将此类资源放置在 ResourceDictionary
中,以便多个窗口可以访问它们。接下来,让我们转到 DataGrid
本身。
基本 DataGrid 操作
正如我一开始所讨论的,大多数应用程序需要支持三个 DataGrid
操作:向网格添加项目,从网格中删除项目,以及在网格中上下移动项目。在我们讨论这些操作之前,让我们先看看网格的样式设置。
DataGrid 样式设置:DataGrid
在 XAML 中进行样式设置。在演示应用程序中,我们将样式放置在主窗口 XAML 的 <Window.Resources>
部分。但在生产应用程序中,更常见的做法是将样式放置在 ResourceDictionary
中,以便在多个窗口之间共享。在复杂的应用程序中,网格可能位于许多不同的程序集中,样式可以放置在位于“共享资源”程序集中的特殊资源字典中。您可以在此处阅读更多相关信息。
样式设置本身非常简单。主窗口包含三种样式:
<Window.Resources>
...
<!-- DataGrid Background -->
<LinearGradientBrush x:Key="BlueLightGradientBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#FFEAF3FF"/>
<GradientStop Offset="0.654" Color="#FFC0DEFF"/>
<GradientStop Offset="1" Color="#FFC0D9FB"/>
</LinearGradientBrush>
<!-- DatGrid style -->
<Style TargetType="{x:Type toolkit:DataGrid}">
<Setter Property="Margin" Value="5" />
<Setter Property="Background" Value="{StaticResource BlueLightGradientBrush}" />
<Setter Property="BorderBrush" Value="#FFA6CCF2" />
<Setter Property="RowBackground" Value="White" />
<Setter Property="AlternatingRowBackground" Value="#FDFFD0" />
<Setter Property="HorizontalGridLinesBrush" Value="Transparent" />
<Setter Property="VerticalGridLinesBrush" Value="#FFD3D0" />
<Setter Property="RowHeaderWidth" Value="0" />
</Style>
<!-- Enable rows as drop targets -->
<Style TargetType="{x:Type toolkit:DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
</Window.Resources>
第一个定义了一个渐变画笔,用作网格背景。第二个样式定义了 DataGrid
的外观,并引用了第一个。第三个样式用于在 DataGrid
中拖放项目,我们将在下面进一步讨论。
总的来说,这些样式是普通的 XAML,不值得在此处进行冗长的讨论。如果您需要更多解释,MSDN 和大多数 WPF 书籍对该主题都有很好的讨论。
向 DataGrid 添加项目:这个任务再简单不过了。当您单击 DataGrid 底部的空白行时,控件会为您创建一个新的空项目并将其添加到其集合中。由于 WPF 数据绑定的魔力,此更改会自动传播到 DataGrid
绑定的视图模型集合。在演示应用程序中,该集合是 GroceryList
属性。当您向网格添加项目时,请注意第一列中的序列号和窗口底部的项目计数。这两个值都取自视图模型属性,而不是网格本身。
DataGrid
通过网格的 ItemsSource
属性绑定到 GroceryList
属性:
<toolkit:DataGrid x:Name="MainGrid" ItemsSource="{Binding
GroceryList}" ...>
DataGrid
的 Sequence
列绑定到集合中每个 GroceryItem
对象的 SequenceNumber
属性:
<toolkit:DataGrid x:Name="MainGrid"
...
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn ... Binding="{Binding SequenceNumber}" ... />
...
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
简而言之,我们所要做的就是设置数据绑定;DataGrid
为我们完成了其余的工作。
从网格中删除项目:这个任务只需要多做一点工作。我们已经在上面讨论过,与 MVVM 实现相关。位于主窗口中 DataGrid
正上方的“删除项目”按钮绑定到视图模型中的 DeleteItem
命令属性。该属性链接到 DeleteItemCommand.cs 类,其 Execute()
方法执行实际工作。
在 DataGrid 中移动项目:这是唯一需要解释的任务。演示应用程序中的实现取自 WPF 开发团队成员 Ben Carter 在 MSDN 上发布的一个代码示例。演示应用程序在 Ben 的实现中添加了放置线,但除此之外,它完全取自他的代码。当您将一个项目拖到网格中的另一行时,您会注意到一条蓝线跟随您的光标在网格中上下移动,指示放置的行将被插入的位置。我之前曾将该功能添加到 WinForms 数据网格中,该实现需要一些相当复杂的自定义绘制代码。在 WPF 中,只需几行简单的格式化代码即可。这只是喜欢 WPF 的又一个理由。
无代码隐藏:在我们深入了解拖放功能如何工作的具体细节之前,请查看 MainWindow.xaml.cs 中的代码。看起来我打破了 MVVM 的一条基本规则:
视图应将其工作委托给其视图模型,而不是依赖代码隐藏。一个设计良好的 MVVM 应用程序在其视图中几乎没有代码隐藏。
显然,在演示应用程序中,演示应用程序的主窗口有相当多的代码隐藏。您可能会想知道这究竟是怎么回事。
我坚信“无代码隐藏”原则,我认为我没有在演示应用程序中违反该原则。无代码隐藏的思想是将控制和业务逻辑从视图移到协调器层,该层在视图和领域模型之间进行调解。
现在再看看主窗口中的代码隐藏。您会注意到所有代码都与数据网格的拖放功能相关。该功能是显示关注点,因此属于视图的范畴。所有代码都不涉及业务逻辑或控制问题。因此,关注点分离得以保留。
在一个更大的应用程序中,有多个视图和许多数据网格,您可能希望将代码的实质内容移除到一个适用于所有视图的服务类中,该服务类将由每个视图中的骨架事件处理程序调用。这种方法将消除重复,并为更新代码提供单一更改点。但在像演示应用程序这样简单的应用程序中,将此类视图支持代码放入代码隐藏中是完全没有问题的。
因此,当您使用 MVVM 开发应用程序时,对无代码隐藏规则要持保留态度。如果您需要编写无法在 XAML 中表达的显示逻辑,那么代码隐藏是一个完全可以接受的地方。
实现拖放:拖放实现非常简单。它有三个元素。首先,我们将拖放所需的事件添加到 DataGrid
的 XAML 中:
<toolkit:DataGrid ...
MouseMove="OnMainGridMouseMove"
DragEnter="OnMainGridCheckDropTarget"
DragLeave="OnMainGridCheckDropTarget"
DragOver="OnMainGridCheckDropTarget"
Drop="OnMainGridDrop"
DataContextChanged="OnMainGridDataContextChanged">
这些事件遵循 .NET 拖放操作的正常约定。如果您需要更多解释,MSDN 对此主题有很好的介绍。
拖放实现的第二组元素是我们在 XAML 中添加的事件的事件处理程序。这些事件处理程序出现在主窗口的代码隐藏中:
#region Static Methods
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
var parent = element;
while (parent != null)
{
var correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
#endregion
#region Event Handlers
private void OnMainGridCheckDropTarget(object sender, DragEventArgs e)
{
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
/* If we are over a row that contains a GroceryItem, set
* the drop-line above that row. Otherwise, do nothing. */
// Set the DragDropEffects
if ((row == null) || !(row.Item is GroceryItem))
{
e.Effects = DragDropEffects.None;
}
else
{
var currentIndex = row.GetIndex();
// Erase old drop-line
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0);
// Draw new drop-line
int direction = (currentIndex - m_OriginalIndex);
if (direction < 0) row.BorderThickness = new Thickness(0, 2, 0, 0);
else if (direction > 0) row.BorderThickness = new Thickness(0, 0, 0, 2);
// Reset old row
m_OldRow = row;
}
}
private void OnMainGridDrop(object sender, DragEventArgs e)
{
e.Effects = DragDropEffects.None;
e.Handled = true;
// Verify that this is a valid drop and then store the drop target
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
if (row != null)
{
m_TargetItem = row.Item as GroceryItem;
if (m_TargetItem != null)
{
e.Effects = DragDropEffects.Move;
}
}
// Erase last drop-line
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0, 0, 0, 0);
}
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
// Exit if shift key and left mouse button aren't pressed
if (e.LeftButton != MouseButtonState.Pressed) return;
if (Keyboard.Modifiers != ModifierKeys.Shift) return;
/* We use the m_MouseDirection value in the
* OnMainGridCheckDropTarget() event handler. */
// Find the row the mouse button was pressed on
var row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
m_OriginalIndex = row.GetIndex();
// If the row was already selected, begin drag
if ((row != null) && row.IsSelected)
{
// Get the grocery item represented by the selected row
var selectedItem = (GroceryItem) row.Item;
var finalDropEffect = DragDrop.DoDragDrop
(row, selectedItem, DragDropEffects.Move);
if ((finalDropEffect == DragDropEffects.Move) && (m_TargetItem != null))
{
/* A drop was accepted. Determine the index of the item being
* dragged and the drop location. If they are different, then
* move the selectedItem to the new location. */
// Move the dragged item to its drop position
var oldIndex = m_ViewModel.GroceryList.IndexOf(selectedItem);
var newIndex = m_ViewModel.GroceryList.IndexOf(m_TargetItem);
if (oldIndex != newIndex) m_ViewModel.GroceryList.Move(oldIndex, newIndex);
m_TargetItem = null;
}
}
}
#endregion
同样,这段代码遵循 .NET 拖放的一般模式,因此我们不再赘述。
在网格上实现拖放线:值得注意的是移动拖放线的实现,它指示放置的行将被插入的位置。演示应用程序通过在光标进入数据行时设置该行的顶部或底部边框,然后在光标进入新行时清除边框来实施拖放线。
进行放置时,WPF DataGrid
将根据拖动方向,将放置的项目插入到鼠标指针下方的行上方或下方:
如果正在拖动项目... | 放下时,项目将插入到... |
...其原始行的上方 | ...鼠标指针下方行的上方 |
...其原始行的下方 | ...鼠标指针下方行的下方 |
因此,首要任务是确定拖动方向。我们从 OnMainGridMouseMove()
事件处理程序获取原始行索引:
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
...
// Find the row the mouse button was pressed on
var row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
m_OriginalIndex = row.GetIndex();
...
}
我们将索引存储在窗口类的成员变量中。接下来,当我们向上或向下拖动行时,我们获取当前行索引。我们从 OnMainGridCheckDropTarget()
事件处理程序(见下文)中获取该索引。然后我们比较这两个值。如果结果是负数,则拖动方向是向上,我们会在鼠标指针下方的行顶部绘制边框。如果结果是正数,则拖动方向是向下,我们会在该行的底部绘制边框。
private void OnMainGridCheckDropTarget(object sender, DragEventArgs e)
{
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
...
var currentIndex = row.GetIndex();
// Erase old drop-line
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0);
// Draw new drop-line
var direction = (currentIndex - m_OriginalIndex);
if (direction < 0) row.BorderThickness = new Thickness(0, 2, 0, 0);
else if (direction > 0) row.BorderThickness = new Thickness(0, 0, 0, 2);
// Reset old row
m_OldRow = row;
...
}
拖放实现的最后一个元素是一个网格样式,它允许网格的行用作放置目标:
<Style TargetType="{x:Type toolkit:DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
与实现的其他元素一样,这里没有什么真正独特或特殊的,所以我们不再进一步评论。
请记住,在拖放操作期间必须按住 Shift 键。演示在 OnMainGridMouseMove()
方法中实现了该功能:
// Exit if shift key and left mouse button aren't pressed
if (e.LeftButton != MouseButtonState.Pressed) return;
if (Keyboard.Modifiers != ModifierKeys.Shift) return;
此功能已添加到演示应用程序中,以简化区分两种类型拖动的过程:
- 用于选择多行的拖动,以及
- 用于将行移动到新位置的拖动。
如果按下 Shift 键,则会拖动选定的行。如果未按下 Shift 键,则会选择多行。
正如您所看到的,WPF DataGrid
中的基本网格操作出奇地容易实现。即使是拖放线的代码也不是很复杂。
结论
正如我们在本文开头所指出的,MVVM 是一种非常可扩展的模式,非常适合简单的应用程序(如演示应用程序)以及更复杂的多程序集应用程序。然而,这并不意味着 MVVM 能够独立支持复杂的企业应用程序。随着应用程序规模的扩大,核心问题是如何将应用程序划分为可管理的块,以及如何将这些块集成到一个单一的、可运行的整体中。
我对 Microsoft 在《WPF 复合应用程序指南》(又称 Prism)中提供的解决方案非常热衷。Prism 是一个应用程序框架,允许开发人员将应用程序划分为模块,这些模块加载到作为应用程序主窗口的外壳中。Prism 使用一些非常巧妙的机制,允许模块之间以及模块与外壳之间进行通信,而不会相互纠缠。
截至本文撰写之时,Prism 已发布到 2.0 版本。与早期的《复合应用程序块》库不同,Prism 并非“全有或全无”的选择。您可以根据需要使用它的多或少。但是 Prism 与 MVVM 的集成非常好,并且设计得如此出色,以至于我发现自己几乎使用了它所提供的一切。在您对 MVVM 感到舒适之后,这绝对是下一步。
至此,我放下笔。我欢迎您对本文提出任何意见或建议;我发现我从读者的评论中学到的东西和撰写文章本身一样多。如果您没有花太多时间在 MVVM 上,我向您推荐它。如果您还没有使用过 WPF DataGrid
,它是一个设计精良、易于使用的控件,与 MVVM 很好地集成。我希望它能像对我一样简化您的开发任务。