简短的版本:你做错了。我的意思是,我怀疑您在某种程度上已经知道这一点,因为代码不起作用。但是看看您说您将拥有 240 个按钮的评论,您真的 这样做是错误的。
此答案旨在引导您完成三种不同的选择,每一种都使您更接近处理这种情况的最佳方法。
从您最初的努力开始,我们可以让您发布的代码大部分按原样工作。您的主要问题是,在成功获得 Grid 的每个 Button 孩子之后,您不能只设置 Button.Background 属性。如果这样做,您将删除在 XAML 中设置的绑定。
相反,您需要重置源数据中的值,然后强制更新绑定目标(因为Settings 对象不提供与 WPF 兼容的属性更改通知机制)。您可以通过将 Reset_Click() 方法更改为如下所示来完成此操作:
private void Reset_Click(object sender, RoutedEventArgs e)
{
Settings.Default.Color0 = Settings.Default.Color1 = Settings.Default.Color2 = "";
Settings.Default.Save();
foreach (Button button in theGrid.Children.OfType<Button>())
{
BindingOperations.GetBindingExpression(button, Button.BackgroundProperty)?.UpdateTarget();
}
}
这并不理想。最好不必直接访问绑定状态,而是让 WPF 处理更新。此外,如果您查看调试输出,则每次将按钮设置为“默认”状态时,都会引发异常。这也不是一个很好的情况。
这些问题都可以解决。首先,通过转向 MVVM 风格的实现,其中程序的状态独立于程序的可视部分存储,可视部分响应该状态的变化。第二,通过添加一些逻辑将无效的string 值强制转换为 WPF 满意的值。
为了实现这一点,制作几个预先制作的帮助类会很有帮助,一个用于直接支持视图模型类本身,一个用于表示命令(这是一种比处理用户输入更好的方法) Click 直接事件)。这些看起来像这样:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class DelegateCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke() ?? true;
}
public void Execute(object parameter)
{
_execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
这些只是示例。 NotifyPropertyChangedBase 类与我日常使用的类基本相同。 DelegateCommand 类是我使用的功能更全面的实现的精简版本(主要是它缺少对命令参数的支持,因为在这个特定场景中不需要它们)。 Stack Overflow 和 Internet 上有很多类似的示例,这些示例通常内置于旨在帮助 WPF 开发的库中。
有了这些,我们可以定义一些“视图模型”类来表示程序的状态。请注意,这些类实际上没有涉及视图本身。一个例外是使用DependencyProperty.UnsetValue,作为对简单性的让步。甚至可以摆脱支持该设计的“强制”方法,正如您将在第三个示例中看到的那样。
首先,一个代表每个单独按钮状态的视图模型:
class ButtonViewModel : NotifyPropertyChangedBase
{
private object _color = DependencyProperty.UnsetValue;
public object Color
{
get { return _color; }
set { _UpdateField(ref _color, value); }
}
public ICommand ToggleCommand { get; }
public ButtonViewModel()
{
ToggleCommand = new DelegateCommand(_Toggle);
}
private void _Toggle()
{
Color = object.Equals(Color, "Green") ? "Red" : "Green";
}
public void Reset()
{
Color = DependencyProperty.UnsetValue;
}
}
然后是一个保存程序整体状态的视图模型:
class MainViewModel : NotifyPropertyChangedBase
{
private ButtonViewModel _button0 = new ButtonViewModel();
public ButtonViewModel Button0
{
get { return _button0; }
set { _UpdateField(ref _button0, value); }
}
private ButtonViewModel _button1 = new ButtonViewModel();
public ButtonViewModel Button1
{
get { return _button1; }
set { _UpdateField(ref _button1, value); }
}
private ButtonViewModel _button2 = new ButtonViewModel();
public ButtonViewModel Button2
{
get { return _button2; }
set { _UpdateField(ref _button2, value); }
}
public ICommand ResetCommand { get; }
public MainViewModel()
{
ResetCommand = new DelegateCommand(_Reset);
Button0.Color = _CoerceColorString(Settings.Default.Color0);
Button1.Color = _CoerceColorString(Settings.Default.Color1);
Button2.Color = _CoerceColorString(Settings.Default.Color2);
Button0.PropertyChanged += (s, e) =>
{
Settings.Default.Color0 = _CoercePropertyValue(Button0.Color);
Settings.Default.Save();
};
Button1.PropertyChanged += (s, e) =>
{
Settings.Default.Color1 = _CoercePropertyValue(Button1.Color);
Settings.Default.Save();
};
Button2.PropertyChanged += (s, e) =>
{
Settings.Default.Color2 = _CoercePropertyValue(Button2.Color);
Settings.Default.Save();
};
}
private object _CoerceColorString(string color)
{
return !string.IsNullOrWhiteSpace(color) ? color : DependencyProperty.UnsetValue;
}
private string _CoercePropertyValue(object color)
{
string value = color as string;
return value ?? "";
}
private void _Reset()
{
Button0.Reset();
Button1.Reset();
Button2.Reset();
}
}
需要注意的重要一点是,以上任何地方都没有尝试直接操作 UI 对象,但是您有所有您需要维护程序状态的东西由用户控制。
有了视图模型,剩下的就是定义 UI:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Width="66" Height="26" Background="{Binding Button0.Color}" Command="{Binding Button0.ToggleCommand}"/>
<Button Width="66" Height="26" Background="{Binding Button1.Color}" Command="{Binding Button1.ToggleCommand}"/>
<Button Width="66" Height="26" Background="{Binding Button2.Color}" Command="{Binding Button2.ToggleCommand}"/>
</StackPanel>
<Button Content="Reset" Width="75" HorizontalAlignment="Right" VerticalAlignment="Bottom" Command="{Binding ResetCommand}"/>
</Grid>
</Window>
这里有几点需要注意:
- MainWindow.xaml.cs 文件中根本没有 代码。它与默认模板完全不同,只有无参数构造函数和对
InitializeComponent() 的调用。通过迁移到 MVVM 风格的实现,许多原本需要的内部管道就完全消失了。
- 此代码不会对任何 UI 元素位置进行硬编码(例如,通过设置
Margin 值)。相反,它利用 WPF 的布局特性将颜色按钮放置在中间的一行中,并将重置按钮放置在窗口的右下方(这样无论窗口大小它都可见)。李>
-
MainViewModel 对象设置为Window.DataContext 值。此数据上下文由窗口中的任何元素继承,除非通过显式设置将其覆盖,或者(如您将在第三个示例中看到的)因为该元素是在不同的上下文中自动生成的。当然,绑定路径都是相对于这个对象的。
现在,如果您真的只有三个按钮,这可能是一个不错的方法。但是使用 240,您会遇到很多复制/粘贴问题。遵循 DRY(“不要重复自己”)原则的原因有很多,包括便利性以及代码的可靠性和可维护性。这一切肯定适用于此。
为了改进上面的 MVVM 示例,我们可以做一些事情:
- 将设置保存在集合中,而不是为每个按钮单独设置属性。
- 维护
ButtonViewModel 对象的集合,而不是为每个按钮提供显式属性。
- 使用
ItemsControl 表示ButtonViewModel 对象的集合,而不是为每个按钮声明一个单独的Button 元素。
要实现这一点,视图模型必须进行一些更改。 MainViewModel 将单个属性替换为单个 Buttons 属性以保存所有按钮视图模型对象:
class MainViewModel : NotifyPropertyChangedBase
{
public ObservableCollection<ButtonViewModel> Buttons { get; } = new ObservableCollection<ButtonViewModel>();
public ICommand ResetCommand { get; }
public MainViewModel()
{
ResetCommand = new DelegateCommand(_Reset);
for (int i = 0; i < Settings.Default.Colors.Count; i++)
{
ButtonViewModel buttonModel = new ButtonViewModel(i) { Color = Settings.Default.Colors[i] };
Buttons.Add(buttonModel);
buttonModel.PropertyChanged += (s, e) =>
{
ButtonViewModel model = (ButtonViewModel)s;
Settings.Default.Colors[model.ButtonIndex] = model.Color;
Settings.Default.Save();
};
}
}
private void _Reset()
{
foreach (ButtonViewModel model in Buttons)
{
model.Reset();
}
}
}
您会注意到Color 属性的处理方式也有些不同。这是因为在此示例中,Color 属性是实际的 string 类型而不是 object,并且我使用 IValueConverter 实现来处理将 string 值映射到 XAML 元素所需的值(稍后会详细介绍)。
新的ButtonViewModel 也有点不同。它有一个新属性,用于指示它是哪个按钮(这允许主视图模型知道按钮视图模型与设置集合的哪个元素一起使用),并且Color 属性处理更简单一些,因为现在我们'只处理string 值,而不是DependencyProperty.UnsetValue 值:
class ButtonViewModel : NotifyPropertyChangedBase
{
public int ButtonIndex { get; }
private string _color;
public string Color
{
get { return _color; }
set { _UpdateField(ref _color, value); }
}
public ICommand ToggleCommand { get; }
public ButtonViewModel(int buttonIndex)
{
ButtonIndex = buttonIndex;
ToggleCommand = new DelegateCommand(_Toggle);
}
private void _Toggle()
{
Color = Color == "Green" ? "Red" : "Green";
}
public void Reset()
{
Color = null;
}
}
使用我们的新视图模型,它们现在可以连接到 XAML:
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding Buttons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Resources>
<l:ColorStringConverter x:Key="colorStringConverter1"/>
<DataTemplate DataType="{x:Type l:ButtonViewModel}">
<Button Width="66" Height="26" Command="{Binding ToggleCommand}"
Background="{Binding Color, Converter={StaticResource colorStringConverter1}, Mode=OneWay}"/>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
<Button Content="Reset" Width="75" HorizontalAlignment="Right" VerticalAlignment="Bottom" Command="{Binding ResetCommand}"/>
</Grid>
</Window>
和以前一样,主视图模型被声明为Window.DataContext 值。但是,我没有显式声明每个按钮元素,而是使用ItemsControl 元素来呈现按钮。它具有以下关键方面:
-
ItemsSource 属性绑定到 Buttons 集合。
- 用于此元素的默认面板是垂直方向的
StackPanel,因此我用水平方向的面板覆盖了它,以实现与前面示例中使用的相同的布局。
- 我已将
IValueConverter 实现的一个实例声明为资源,以便可以在模板中使用它。
- 我已将
DataTemplate 声明为资源,并将DataType 设置为ButtonViewModel 的类型。当呈现单个ButtonViewModel 对象时,WPF 将在范围内的资源中查找分配给该类型的模板,并且由于我在这里声明了一个,它将使用它来呈现视图模型对象。对于每个ButtonViewModel 对象,WPF 将在DataTemplate 元素中创建一个内容实例,并将该内容的根对象的DataContext 设置为视图模型对象。最后,
- 在模板中,绑定使用我之前声明的转换器。这允许我在属性绑定中插入一点 C# 代码,以确保正确处理
string 值,即当它为空时,使用适当的 DependencyProperty.UnsetValue,避免来自绑定引擎的任何运行时异常.
这是转换器:
class ColorStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string text = (string)value;
return !string.IsNullOrWhiteSpace(text) ? text : DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
在这种情况下,ConvertBack() 方法没有实现,因为我们只会在OneWay 模式下使用绑定。我们只需要检查string 的值,如果它为空或空(或空格),我们将返回DependencyProperty.UnsetValue。
关于此实现的一些其他说明:
- Settings.Colors 属性设置为类型
System.Collections.Specialized.StringCollection,并使用三个空的string 值初始化(在设计器中)。这个集合的长度决定了创建了多少个按钮。当然,如果您更喜欢其他方式,您可以使用任何您想要跟踪数据的机制。
- 对于 240 个按钮,简单地将它们排列在水平行中可能对您有用,也可能对您不起作用(取决于按钮的实际大小)。您可以将其他面板对象用于
ItemsPanel 属性;可能的候选者包括UniformGrid 或ListView(带有GridView 视图),它们都可以在自动间隔的网格中排列元素。