短版:你做錯了。我的意思是,我懷疑你在某種程度上已經知道,因爲代碼不起作用。但看看你的評論說,你會有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>
有些東西這裏要注意:
- 有沒有代碼都在主窗口.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
也有點不同。它有一個新屬性,用於指示它是哪個按鈕(這允許主視圖模型知道按鈕視圖模型的設置集合中包含哪個元素),而屬性處理更簡單一些,因爲現在我們只有string
值處理,而不是DependencyProperty.UnsetValue
值也一樣:前
<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>
,主視圖:
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.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
視圖),兩者都可以將元素排列在自動間隔網格中。
這些按鈕直接添加在主WPF .. 內。我嘗試將所有這些(240個按鈕)放置在一個堆疊面板中,但是它們的位置很混亂。 –
PaperClip
我嘗試命名網格,如你的例子,並使用foreach方法,但我產生一個錯誤,說「網格名稱」不存在於當前的情況下 – PaperClip
你真的給網格一個x:名稱和實現的foreach循環在同一窗口的代碼隱藏?那麼它應該工作。請發佈您的所有示例代碼以提供您的觀點。 – mm8