MVVM 视图代码隐藏的三种情况






1.95/5 (7投票s)
三种情况说明了为什么需要在 MVVM 中考虑视图的代码隐藏。
引言
本文重点介绍了在 MVVM 应用程序中,在视图中包含代码隐藏的三个常见且有用的目的。
背景
写完我的第一篇文章后,我收到了两个差评,一个一星,一个三星,我感到非常沮丧。 反思了文章之后,我想到一个可能的原因是我关于视图应该包含相对代码隐藏的论述。 我查阅了一些其他文章,发现“零代码隐藏”的纯粹主义观念非常普遍,所以我想为这一点进行一些辩护……
纯粹主义者
纯粹主义者的想法是任何代码隐藏都应该避免。 他们认为代码隐藏会阻碍测试并混淆逻辑。 如果视图代码隐藏仅限于视图逻辑,那么这两种看法都是不正确的。
当视图逻辑被错误地放在视图模型中时,纯粹主义的方法实际上会引起问题。 视图模型因此会与视图耦合,而这正是 MVVM 试图避免的第一件事。 如果一个控件特定的属性没有连接到视图模型,那么视图模型可能无法在其他控件中重用。
情况 1 - CollectionViewSources
尽管 WPF 已经存在七年左右了,但 WPF 开发者中仍然有许多人不知道 CollectionViewSource 的细节。 它本质上是一个包装了各种可枚举项的包装器,WPF 实际上是绑定到它的。 它提供了对枚举、选择、过滤和排序的支持。
CollectionViewSource 的主要好处是可以过滤和排序,而无需更改实际列表。 对于非常大的列表或可能加载成本很高的视图模型,它提供了显著的性能优势。
以下是如何使用它的一个例子
namespace TestProject
{
public class WidgetViewModel
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
public class WidgetCollectionViewModel
{
private List<WidgetViewModel> _allWidgets;
public List<WidgetViewModel> AllWidgets
{
get
{
if (_allWidgets == null)
{
_allWidgets = new List<WidgetViewModel>()
{
new WidgetViewModel() { Name = "Active Widget", IsActive = true },
new WidgetViewModel() { Name = "Inactive Widget", IsActive = false }
};
}
return _allWidgets;
}
}
}
}
<Window x:Class="TestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestProject"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ListBox x:Name="WidgetListBox"
DisplayMemberPath="Name"
ItemsSource="{Binding AllWidgets}"/>
<CheckBox x:Name="IsActiveOnlyCheckBox"
Content="Active Only:"/>
</StackPanel>
</Window>
namespace TestProject
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new WidgetCollectionViewModel();
DataContext = vm;
CollectionViewSource.GetDefaultView(vm.AllWidgets).Filter += item =>
{
var widget = item as WidgetViewModel;
if (widget != null)
{
return !(IsActiveOnlyCheckBox.IsChecked ?? false) || widget.IsActive;
}
return true;
};
IsActiveOnlyCheckBox.Click +=
(o, e) => { CollectionViewSource.GetDefaultView(vm.AllWidgets).Refresh(); };
}
}
}
当设置过滤器时,它会运行,但正如您可能看到的,没有钩子来通知过滤器条件何时改变。 因此,当过滤器条件更改时,例如 IsActiveCheckBox,您需要调用 Refresh()。 每次调用 Refresh() 时,它都会运行 Filter 来确定应该显示什么。
如果我在视图模型中创建了一个过滤器属性,或者根据 UI 选择修改了列表,我可能会影响其他业务流程。 在这种情况下,过滤和排序是仅属于视图的职责,其中绝对不涉及任何业务逻辑。 它也是一种常见的策略,可以根据易于选择的字段(例如用于活动记录的复选框)来减少显示的项目数。
如果我的 WidgetViewModel 具有保存命令或任何其他业务逻辑,那么上面的代码丝毫不会影响其自动化测试或单元测试的能力。 如果用户想要以不同的方式对列表进行排序,或者程序通过用户控件自动选择特定的小部件,情况也是如此——所有这些功能都由 CollectionViewSource 提供。
情况 2 - 导出 UI 数据
导出 UI 数据可能需要大量时间。 这可以通过多种方式实现,例如打印图表、将网格导出到 Excel 以及保存文件。 通常,在这些操作期间,您希望阻止对控件的访问,同时显示某种类型的进度条。
下面的示例演示了如何实现该设计的。
<Window x:Class="TestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestProject"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DataGrid x:Name="MyDataGrid">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" />
</DataGrid.Columns>
</DataGrid>
<Rectangle x:Name="WorkingRectangle"
Fill="Gray"
Opacity=".3"
Visibility="Collapsed">
</Rectangle>
<Border x:Name="WorkingMarqueeBorder"
BorderBrush="SteelBlue"
Background="LightBlue"
BorderThickness="5"
CornerRadius="5"
Height="50"
Width="170"
Visibility="Collapsed">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="Working..."
Foreground="White"
HorizontalAlignment="Center"/>
<ProgressBar IsIndeterminate="True"
Height="20"
Width="150"/>
</StackPanel>
</Border>
</Grid>
<Button x:Name="ExportButton"
Grid.Row="1"
Content="Export"
Click="ExportButton_Click"/>
</Grid>
</Window>
namespace TestProject
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ExportButton_Click(object sender, RoutedEventArgs e)
{
ExportButton.IsEnabled = false;
StartWorking();
new TaskFactory().StartNew(() =>
{
// Normally this would be a process such as exporting the grid data
// (potentially thousands of rows) or saving a chart as an image, but
// for simplicity this is just sleeping to simulate work.
Thread.Sleep(3000);
Application.Current.Dispatcher.Invoke(() =>
{
StopWorking();
ExportButton.IsEnabled = true;
});
}, TaskCreationOptions.LongRunning);
}
private void StartWorking()
{
WorkingRectangle.Visibility = System.Windows.Visibility.Visible;
WorkingMarqueeBorder.Visibility = System.Windows.Visibility.Visible;
}
private void StopWorking()
{
WorkingRectangle.Visibility = System.Windows.Visibility.Collapsed;
WorkingMarqueeBorder.Visibility = System.Windows.Visibility.Collapsed;
}
}
}
情况 3 - 用户设置
对于高级应用程序,UI 的可自定义部分通常保存为用户设置。 虽然用户设置的管理是视图模型和业务逻辑的职责,但设置数据本身特定于视图元素,例如布局、放置和一般数据表示。
网格控件也很适合讨论此功能,因为网格很复杂(分组、排序、列排序、列间距等),并且布局序列化通常由第三方控件开箱即用支持。 您还有对接位置、默认文件夹路径、窗口大小等。 重点是,在所有这些示例中,没有任何一项是由业务逻辑决定的。
不可避免地,设置数据必须从 UI 本身提取,并且并非所有数据都可以通过绑定获得。 在许多情况下,唯一的提取数据的方法是调用视图对象的上的方法。 虽然可以通过命令传递控件以仅仅为了调用其代码隐藏之外的方法,但这太荒谬了……
摘要
零代码隐藏方法的主要陷阱不是存在一些显然不属于业务逻辑的代码,而是它会让你陷入将所有内容都塞进视图模型逻辑的思维定势,而不管范围如何。 除了代码隐藏的独特功能外,您还有命令、转换器和模板选择器,它们具有独特且非常有用的目的。 如果您忽略这些工具及其预期用途,它可能会使开发比实际需要更加困难——无论是现在还是长远来看。
奖励部分:本地时间转换器
视图模型在 UTC 和本地时间之间来回转换是很常见的,但所有业务逻辑都应专门保留在 UTC 中。 使用此标准可以减少混淆,简化代码,并减少错误。
每当您需要显示 DateTime 时,只需连接到一个本地时间转换器类即可。 下面是一个示例。
public class LocalTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var utcTime = value as DateTime?;
if (utcTime != null)
{
return DateTime.SpecifyKind(utcTime.Value, DateTimeKind.Utc).ToLocalTime();
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var localTime = value as DateTime?;
if (localTime != null)
{
return DateTime.SpecifyKind(localTime.Value, DateTimeKind.Local).ToUniversalTime();
}
return value;
}
}
如果时区是用户偏好设置,并且可能不对应于本地系统时间,那么也可以在此处包含它。 转换假定传递的类型正确,但当然您可以更改行为以根据需要执行验证。
在 IValueConverter 实现中,ConvertBack 通常不实现;然而,在这种情况下,实现它非常重要,因为您将需要从各种控件中选择 DateTime 来设置视图模型属性。 为此,您需要转换回 UTC。
我想提到这个特定的案例,因为它是一个简单的例子,说明即使您可以将代码放入视图模型,通过利用适当的工具也有更简单的方法。 简单并不意味着走捷径。 转换器是一个单一职责对象,可以统一地转换时间——您无法做得更好。