静态属性的动态绑定






4.92/5 (14投票s)
如何实现静态属性的动态绑定。
引言
在 WPF 中,有时我们希望绑定到静态值,而不是添加到与 DataContext
关联的 ViewModel
中。通常,我们希望绑定的静态值是 Settings 文件中的值。绑定到这些值没有问题;问题在于当值发生变化时;项目 Settings 不支持 INotifyPropertyChanged
接口,因此也不支持响应 PropertyChanged
事件。因此,如果属性发生更改,这些更改不会传播到 View
。
背景
过去,我曾将这些静态变量添加到 ViewModel
中,以便能够绑定到属性,但这些变量实际上不应成为 ViewModel
的一部分,因为它们与底层的 Model
没有关系。在我开始研究这个想法的具体案例中,我正在研究如何更好地实现一个我难以维护的代码设计。我有一组值存储在 Settings
文件中,这些值与保存的目录路径以及与目录路径恢复或选择默认目录相关的枚举相关。使之令人困惑的是,对于不同的目录路径,存在许多针对错误值的备用选项。为了提高可维护性,我并没有真正将这些信息从 ViewModel
中移除,但由于我已经在进行更改,我决定研究如何将这些信息从 ViewModel
中移除。我还想将绑定命令放在与可绑定属性相同的类中。
实现
我为本文创建的示例非常简单。它包含一个包含路径的 TextBox
,以及一个按钮,让用户浏览到一个新文件夹,该文件夹将显示在 TextBox
中。
首先显而易见的是,我需要一个 Adapter
类来连接 Settings 和 View。第一个问题是我需要绑定到一个静态类,否则我需要将该类与窗口的 DataContext
关联,这无法达到目的。静态类的问题在于它无法实现 INotifyPropertyChanged
。解决方案是有一个静态属性,该属性返回实现 INotifyPropertyChanged
的类的实例,这是一个单例。我使用同一个类来处理静态和实例代码。以下代码使用静态属性创建并返回一个实例
public class DirectoryManager : INotifyPropertyChanged
{
private static DirectoryManager _instance = new DirectoryManager();
public static DirectoryManager Instance { get { return _instance; } }
private DirectoryManager() { }
public event PropertyChangedEventHandler PropertyChanged;
}
请注意,我还将类的默认实例构造函数设为 private
,以确保它是一个单例,当然,该类继承自 INotifyPropertyChanged
并定义了必需的 PropertyChanged
事件。
用于显示目录路径的公共属性是一个用于绑定的普通属性,并带有用于从 Settings 文件获取和保存字符串的附加代码
public string DirectoryPath
{
get { return Properties.Settings.Default.DirectoryPath; }
set
{
if (Properties.Settings.Default.DirectoryPath != value)
{
Properties.Settings.Default.DirectoryPath = value;
Properties.Settings.Default.Save();
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("DirectoryPath"));
}
}
}
这一切都非常直接(通常我不会在属性中直接处理 PropertyChanged
事件,但对于示例,我更喜欢这样做)。困难的部分是连接 Browse 按钮的支持。
不幸的是,无法为按钮使用 ICommand
接口;似乎 Microsoft 没有为 Source 绑定(它提供了绑定静态变量的能力)提供 Command 绑定的支持。因此,我必须改用 RoutedUICommand
。这绝对比普通的 Command 绑定复杂,我并不喜欢(我希望看到一种更简单的实现方式,用于绑定事件,而不是 ICommand
),但它有效,并且有一个简单的示例来演示如何实现 RoutedUICommand
并不坏。在 DirectoryManager
类中,必须包含以下代码来创建 RoutedUICommand
public static RoutedUICommand BrowseCommand
{
get { return _browseCommand; }
}
public static void BrowseCommand_Executed(object sender,
ExecutedRoutedEventArgs e)
{
var path = DirectoryManager.Browse(_instance.DirectoryPath,
"Select Directory");
if (path != null)
_instance.DirectoryPath = path;
}
public static void BrowseCommand_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
static DirectoryManager()
{
_browseCommand = new RoutedUICommand("Browse for Directory",
"BrowseCommand", typeof(DirectoryManager));
}
正如所见,我们需要一个属性来返回 BrowseCommand
,并在类构造函数中实例化该命令。
此外,我们需要定义两个方法来实现命令的 Execute
和 CanExecute
方法。这些方法的名称并不重要,因为有另一段代码会将这些方法与命令关联起来
public static void BindCommands(System.Windows.Window window)
{
window.CommandBindings.Add(new CommandBinding(BrowseCommand,
BrowseCommand_Executed, BrowseCommand_CanExecute));
}
据说还有一种方法可以使用 XAML 创建绑定,但这似乎不起作用,所以我们必须在代码中进行绑定。
为了能够在窗口中使用该命令,我们需要在 Window
的代码隐藏中调用此方法
namespace BindableSettingsExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DirectoryManager.BindCommands(this);
}
}
}
现在,可以创建 XAML,它提供了对 Settings 文件的绑定,以及 Browse 按钮的命令绑定。
<Window x:Class="BindableSettingsExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:bindableSettings="clr-namespace:BindableSettingsExample"
Title="Bindable Settings Example" Height="300" Width="525">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="75"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Margin="0,0,2,-5"
HorizontalAlignment="Left"
Content="Directory Path:"/>
<TextBox Grid.Column="0" Grid.Row="1" Name="txtDirectoryPath"
Height="22" Margin="6,0,0,5"
Text="{Binding DirectoryPath,Source={x:Static
bindableSettings:DirectoryManager.Instance}, Mode=TwoWay}"
IsReadOnly="True"/>
<Button Grid.Column="1" Grid.Row="1" Content="_Browse" Height="22"
Padding="5,2,5,2" Margin="5,0,7,5"
Command="bindableSettings:DirectoryManager.BrowseCommand"/>
</Grid>
</Window>
如所见,TextBox
文本的绑定只是
{Binding DirectoryPath,Source={x:Static
bindableSettings:DirectoryManager.Instance}, Mode=TwoWay}
按钮命令的绑定只是
bindableSettings:DirectoryManager.BrowseCommand
结论
此实现用于绑定到 Settings 文件,但设计实际上更加灵活。该设计可以与未保存在 Settings 文件中的数据,甚至仅与会话相关的数据一起使用。还可以将静态实例更改为与应用程序状态相关的不同类。这可以在不弄乱 DataContext
的 ViewModel
的情况下完成,其中包含与 Model 无关的信息。