C# 使用按钮更改多个控件的后台属性

C# 使用按钮更改多个控件的后台属性,c#,wpf,xaml,button,settings,C#,Wpf,Xaml,Button,Settings,我正在开发一个在主窗口上有很多按钮的应用程序 这些按钮已单独编程,可在按下时更改颜色,并使用Visual Studio中的用户设置保存这些颜色 更确切地说,当用户按下一次按钮时,其背景变为红色,当用户再次按下按钮时,背景变为绿色 为mm8编辑: <StackPanel x:Name="thePanel"> <Button x:Name="Button0" HorizontalAlignment="Left" Margin="197,139,0,0" VerticalAl

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

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

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

为mm8编辑:

<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>
以下是xaml(示例):

或者使用“Controls”(this.Controls等)的任何其他方法,我用红色下划线,表示控件类没有定义


我做错什么了吗?你们有什么建议,我可以如何编程,将所有按钮的背景更改为默认值

由于
按钮
元素位于某种父
面板
中,例如
堆栈面板
,因此您可以像这样迭代其子
集合:

foreach(Button button in thePanel.Children.OfType<Button>())
{
    //...
}
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();
    }
}
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);
    }
}
foreach(类型()面板中的Children.OfType()按钮)
{
//...
}
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>

由于
按钮
元素位于某种父
面板
中,例如
堆栈面板
,因此您可以像这样迭代其子
集合:

foreach(Button button in thePanel.Children.OfType<Button>())
{
    //...
}
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();
    }
}
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);
    }
}
foreach(类型()面板中的Children.OfType()按钮)
{
//...
}
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个按钮,你真的走错了方向

这个答案旨在引导您通过三个不同的选项,每一个选项都使您更接近于处理此场景的最佳方法

从您最初的努力开始,我们可以让您发布的代码按原样工作。您的主要问题是,在成功获得
网格
的每个
按钮
子级后,您不能只设置
按钮.Background
属性。如果这样做,您将删除在XAML中设置的绑定

相反,您需要重置源数据中的值,然后强制更新绑定目标(因为
设置
对象不提供与WPF兼容的属性更改通知机制)。您可以通过将
Reset\u Click()
方法更改为如下所示来完成此操作:

foreach(Button button in thePanel.Children.OfType<Button>())
{
    //...
}
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();
    }
}
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
类是我使用的功能更全面的实现的精简版本(主要是,它缺少对命令参数的支持,因为在这个特定场景中不需要它们)。在堆栈溢出和Internet上有许多类似的例子,它们通常内置在一个库中,用于帮助WPF开发

有了这些,我们可以定义一些“视图模型”类来表示程序的状态。请注意,这些类实际上没有任何涉及视图本身的内容。一个例外是使用
dependencProperty.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>
您会注意到
Color
属性的处理也有点不同。这是因为在本例中,
Color
属性是一个实际的
string
类型,而不是
object
,我使用
IValueConverter
实现来处理
string
值与XAML元素所需值的映射(稍后将详细介绍)

新的
按钮viewmodel
也有点不同。它有一个新的属性,用于指示它是哪个按钮(这允许主视图模型知道按钮视图模型所使用的设置集合的哪个元素),而
颜色
属性处理稍微简单一些,因为现在我们只处理
字符串
值,而不是
dependencProperty.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>
在这种情况下,不会实现
ConvertBack()
方法,因为我们只会在
单向
模式下使用绑定。我们只需要检查
字符串
值,如果它为null或空(或空白),则返回
dependencProperty.UnsetValue

关于这一实施的一些其他说明:

  • Settings.Colors属性设置为type
    System.Collections.Specialized.StringCollection
    ,并使用三个空的
    string
    值初始化(在设计器中)。此集合的长度决定了创建的按钮数量。当然,如果您喜欢其他方式,您可以使用任何想要跟踪数据这一方面的机制
  • 对于240个按钮,简单地将它们排列成水平行可能对您有用,也可能不适用(取决于按钮的实际大小)。您可以为
    ItemsPanel
    属性使用其他面板对象;可能的候选者包括
    UniformGrid
    ListView
    (带