使用按钮更改多个控件的背景属性

Changing background proprieties of multiple controls using a button

我正在开发一个在主 window 上有很多按钮的应用程序。

按钮已单独编程为在按下时改变颜色,并使用 Visual Studio 中的用户设置保存这些颜色。

更准确地说,当用户按下按钮一次时,其背景变为红色,而当他再次按下按钮时,背景变为绿色。

为 mm8 编辑:

这是 xaml(示例):

<Window x:Class="test2.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:local="clr-namespace:test2"
    xmlns:properties="clr-namespace:test2.Properties"
    mc:Ignorable="d"
    Title="MainWindow" WindowStartupLocation="CenterScreen" Height="850" Width="925">
    <Grid x:Name="theGrid">
        <Button x:Name="Button0" HorizontalAlignment="Left" Margin="197,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color0, Mode=TwoWay}" Click="Button0_Click"/>
        <Button x:Name="Button1" HorizontalAlignment="Left" Margin="131,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color1, Mode=TwoWay}" Click="Button1_Click"/>
        <Button x:Name="Button2" HorizontalAlignment="Left" Margin="263,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color2, Mode=TwoWay}" Click="Button2_Click"/>
        <Button x:Name="Reset" Content="Reset" HorizontalAlignment="Left" Margin="832,788,0,0" VerticalAlignment="Top" Width="75" Click="Reset_Click" />


    </Grid>
</Window>

这是我在每个按钮的点击事件中实现的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;


namespace test2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button0_Click(object sender, RoutedEventArgs e)
        {
            if (Properties.Settings.Default.Color0 == "Green")
            {
                Properties.Settings.Default.Color0 = "Red";
                Properties.Settings.Default.Save();
            }
            else
            {
                Properties.Settings.Default.Color0 = "Green";
                Properties.Settings.Default.Save();
            }
        }

        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            if (Properties.Settings.Default.Color1 == "Green")
            {
                Properties.Settings.Default.Color1 = "Red";
                Properties.Settings.Default.Save();
            }
            else
            {
                Properties.Settings.Default.Color1 = "Green";
                Properties.Settings.Default.Save();
            }
        }

        private void Button2_Click(object sender, RoutedEventArgs e)
        {
            if (Properties.Settings.Default.Color2 == "Green")
            {
                Properties.Settings.Default.Color2 = "Red";
                Properties.Settings.Default.Save();
            }
            else
            {
                Properties.Settings.Default.Color2 = "Green";
                Properties.Settings.Default.Save();
            }
        }
        private void Reset_Click(object sender, RoutedEventArgs e)
        {
            foreach (Button button in theGrid.Children.OfType<Button>())
      }
    }
}

现在,我想要某种重置按钮,按下该按钮会将所有按钮的背景更改为默认设置(不是红色,也不是绿色)。

我试图做的是使用来自 this 线程的想法并将它们用作重置按钮上的点击事件,但是每当我这样做时

foreach (Control x in Control.Controls)

或使用 "Controls" 的任何其他方法(this.Controls,等等)我用红色下划线表示它,说控件 class 没有定义。

我是不是做错了什么?你们对我如何对该按钮进行编程以将所有按钮的背景更改为默认值有什么建议吗?

由于 Button 元素位于某种父 Panel 中,例如 StackPanel,您可以像这样遍历其 Children 集合:

foreach(Button button in thePanel.Children.OfType<Button>())
{
    //...
}

XAML:

<StackPanel x:Name="thePanel">
    <Button x:Name="Button0" HorizontalAlignment="Left" Margin="197,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color0, Mode=TwoWay}" Click="Button0_Click"  />
    <Button x:Name="Button1" HorizontalAlignment="Left" Margin="131,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color1, Mode=TwoWay}" Click="Button1_Click" />
    <Button x:Name="Button0_Copy" HorizontalAlignment="Left" Margin="563,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Color_0, Mode=TwoWay, Source={x:Static properties:Settings.Default}}" Click="Button0_Copy_Click"/>
    <Button x:Name="Button1_Copy" HorizontalAlignment="Left" Margin="497,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Color_1, Mode=TwoWay, Source={x:Static properties:Settings.Default}}" Click="Button1_Copy_Click"/>
</StackPanel>

简短版本:你做错了。我的意思是,我怀疑你在某种程度上已经知道了,因为代码不起作用。但是看看你说你将有 240 个按钮的评论,你 真的 做错了。

此答案旨在引导您完成三种不同的选择,每一种都会让您更接近处理这种情况的最佳方法。

从您最初的努力开始,我们可以让您发布的代码大部分工作 as-is。您的主要问题是,在成功获得 Grid 的每个 Button child 之后,您不能只设置 Button.Background 属性。如果这样做,您将删除在 XAML.

中设置的绑定

相反,您需要重置源数据中的值,然后强制更新绑定目标(因为 Settings object 不提供 WPF-compatible property-changed通知机制)。您可以通过将 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 处理更新。此外,如果您查看调试输出,每次将按钮设置为 "default" 状态时,都会引发异常。这也不是一个很好的情况。

这些问题是可以解决的。首先,通过移动到 MVVM-style 实现,其中程序的状态独立于程序的可视部分存储,可视部分响应该状态的变化。第二,通过添加一些逻辑将无效的 string 值强制转换为 WPF 满意的值。

为了完成这个,制作一对 pre-made 助手 classes 是很有帮助的,一个直接支持视图模型 classes 本身,一个代表一个命令(这是一种比直接处理 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 class 与我在 day-to-day 基础上使用的基本相同。 DelegateCommand class 是我使用的更多 fully-featured 实现的 stripped-down 版本(主要是,它缺少对命令参数的支持,因为在这个特定场景中不需要它们). Stack Overflow 和 Internet 上有很多类似的示例,通常内置于旨在帮助 WPF 开发的库中。

有了这些,我们可以定义一些 "view model" classes 来表示程序的状态。请注意,这些 classes 实际上 nothing 涉及视图本身。一个例外是使用 DependencyProperty.UnsetValue 作为对简单性的让步。连同支持该设计的 "coerce" 方法一起,也可以去掉它,正如您将在第三个示例中看到的那样,在这个示例之后。

首先,表示每个按钮状态的视图模型:

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 object,但你拥有 一切在那里,您需要维护由用户控制的程序状态。

有了视图模型,剩下的就是定义 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>

这里需要注意的几点:

  1. MainWindow.xaml.cs 文件中根本没有 代码。它与默认模板完全没有变化,只有无参数构造函数和对 InitializeComponent() 的调用。通过转向 MVVM-style 实现,许多内部管道所需的其他功能将完全消失。
  2. 此代码不 hard-code 任何 UI 元素位置(例如通过设置 Margin 值)。相反,它利用 WPF 的布局功能将彩色按钮放在中间的一行中,并将重置按钮放在 window 的右下方(这样无论 [= 的大小如何,它都是可见的) 198=] 是).
  3. MainViewModel object 设置为 Window.DataContext 值。此数据上下文由 window 中的任何元素继承,除非通过显式设置它来覆盖,或者(如您将在第三个示例中看到的那样)因为该元素是在不同的上下文中自动生成的。当然,绑定路径都是相对于此object。

现在,如果您真的只有三个按钮,这可能是一个不错的方法。但是对于 240,您会遇到很多 copy/paste 令人头疼的问题。遵循 DRY ("don't repeat yourself") 原则的原因有很多,包括便利性以及代码的可靠性和可维护性。这一切肯定适用于此。

为了改进上面的 MVVM 示例,我们可以做一些事情:

  1. 将设置保存在一个集合中,而不是为每个按钮单独设置 属性。
  2. 维护 ButtonViewModel 的集合objects 而不是每个按钮都有明确的 属性。
  3. 使用 ItemsControl 来呈现 ButtonViewModel object 的集合,而不是为每个按钮声明一个单独的 Button 元素。

为此,视图模型必须稍作更改。 MainViewModel 将单个属性替换为单个 Buttons 属性 以容纳所有按钮视图模型 objects:

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 元素来显示按钮。它具有以下关键方面:

  1. ItemsSource 属性 绑定到 Buttons 集合。
  2. 用于该元素的默认面板将是 vertically-oriented StackPanel,因此我用 horizontally-oriented 覆盖了它,以实现与之前相同的布局示例。
  3. 我已经将我的 IValueConverter 实现的实例声明为资源,以便它可以在模板中使用。
  4. 我已经将 DataTemplate 声明为资源,DataType 设置为 ButtonViewModel 的类型。在呈现单个 ButtonViewModel object 时,WPF 将在 in-scope 资源中查找分配给该类型的模板,并且由于我已在此处声明了一个模板,因此它将使用它来呈现视图模型 object。对于每个 ButtonViewModel object,WPF 将在 DataTemplate 元素中创建一个内容实例,并将为该内容的根 object 设置 DataContext到视图模型 object。最后,
  5. 在模板中,绑定使用了我之前声明的转换器。这允许我在 属性 绑定中插入一点 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 值,如果它为 null 或空(或空白),我们 return 而是 DependencyProperty.UnsetValue

关于此实现的一些其他说明:

  1. Settings.Colors 属性 设置为 System.Collections.Specialized.StringCollection 类型,并使用三个空 string 值初始化(在设计器中)。该集合的长度决定了创建的按钮数量。当然,如果您喜欢其他方式,您可以使用任何您想要跟踪数据这边的机制。
  2. 对于 240 个按钮,将它们简单地排成水平行可能对您有效,也可能无效(取决于按钮的实际大小)。您可以将其他面板 object 用于 ItemsPanel 属性;可能的候选对象包括 UniformGridListView(使用 GridView 视图),它们都可以将元素排列在自动间隔的网格中。