我是 WPF 开发新手,正在尝试为复杂的参数化模型制作用户界面。在我的 ViewModel 中,有我编写的 Configuration 类的两个实例:每次用户与界面交互时,WorkingConfig 都会立即更新,而 SavedConfig 仅当用户按下“计算”按钮时才会更新。
我的大部分用户界面由成对的文本块和各种简单的输入元素堆叠组成,例如文本框、组合框和复选框。每当用户更改输入元素之一中的值并因此更改工作配置时,如果新值不等于 SavedConfig 中存储的值,我希望相应的 TextBlock 更改其样式。因此,用户可以直观地跟踪他们更改了哪些值,而无需重新计算。
要为每个 TextBlock 执行此操作似乎涉及大量复制粘贴代码,只是为了为每个 TextBlock 设置不同的绑定,因此我试图找到使用样式的通用解决方案。通过一些挖掘和一些 ChatGPT,我设置了一个与要检查的属性名称相对应的附加属性,以及一个 MultiValueConverter 来检查 WalkingConfig 和 SavedConfig 的命名属性的值:
public static class AttachedProperty
{
public static readonly DependencyProperty AttributeNameProperty = DependencyProperty.RegisterAttached("AttributeName",typeof(string), typeof(AttachedProperty));
public static string GetAttributeName(DependencyObject obj)
{
return (string)obj.GetValue(AttributeNameProperty);
}
public static void SetAttributeName(DependencyObject obj, string value)
{
obj.SetValue(AttributeNameProperty, value);
}
}
public class AttributeComparisonConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 3 && values[0] is IConfiguration config1 && values[1] is IConfiguration config2 )
{
string AttributeName = values[2]?.ToString();
if (string.IsNullOrEmpty(AttributeName)) { return true; }
return string.Equals(GetAttributeValue(config1, AttributeName), GetAttributeValue(config2, AttributeName));
}
return false;
}
private string GetAttributeValue(IConfiguration config, string AttributeName)
{
return config?.GetType().GetProperty(AttributeName).GetValue(config)?.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
在样式中,我绑定了配置元素和应检查所有 TextBlock 的附加属性。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:core="clr-namespace:my_core_namespace">
<core:AttributeComparisonConverter x:Key="AttributeComparisonConverter"/>
<Style BasedOn="{StaticResource {x:Type TextBlock}}"
TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="DarkSlateGray"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="Margin" Value="15, 10, 0, 5"/>
<Style.Triggers>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource AttributeComparisonConverter}">
<Binding Path="WorkingConfig"/>
<Binding Path="SavedConfig"/>
<Binding Path="(core:AttachedProperty.AttributeName)" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
然后在给定的 UserControl 中,我只需分配应为每个 TextBlock 检查的属性的名称:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="example 1" core:AttachedProperty.AttributeName="property1"/>
<TextBlock Text="example 2" core:AttachedProperty.AttributeName="attribute2"/>
<TextBlock Text="example 3" core:AttachedProperty.AttributeName="attribute3"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBox x:Name="textBox_1" Text="{Binding WorkingConfig.attribute1}"/>
<CheckBox x:Name="checkBox_1" Text="{Binding WorkingConfig.attribute2}"/>
<ComboBox x:Name="comboBox_1" Text="{Binding WorkingConfig.attribute3}"/>
</StackPanel>
</Grid>
这似乎有点工作,只不过比较仅在创建 UserControl 时触发,而不是每次更新值时触发。据我了解,每次更新受监控的属性时,我的 DataTrigger 中都没有实际触发的内容。但我不确定如何以通用方式改变它。
很抱歉这篇文章很长,我希望我的问题可以理解。预先感谢您的任何提示!
如果我正确理解你的解释。您有两个具有相同嵌套属性的源属性“WorkingConfig”和“SavedConfig”。使用 INotifyPropertyChanged.PropertyChanged 向嵌套属性通知其更改。如果没有,那么只有在保存数据后替换“SavedConfig”属性中的实例并为“SavedConfig”属性实现 INotifyPropertyChanged.PropertyChanged 才能实现您的任务。
对于任务本身,您想要获取某个布尔值,该值指示同名的“WorkingConfig”和“SavedConfig”属性之间的差异。您可以在“AttachedProperty.AttributeName”属性中设置属性名称。
在我看来,您将需要另外两个属性来实现,因为无法在 XAML 中创建您所需的绑定。
一个属性将接收值“WorkingConfig”和“SavedConfig”。此属性将使用值比较转换器在代码中创建绑定。然后它将将此绑定添加到第二个属性,该属性的值将决定您的样式。
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
namespace CommonCore.Converters
{
[ValueConversion(typeof(object), typeof(Array))]
public class ToArrayConverter : IValueConverter, IMultiValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return new object[] { value };
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
object[] copy = new object[values.Length];
values.CopyTo(copy, 0);
return copy;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
// Если нет членов экземпляра, то нет и смысла в конструкторе экземпляра.
private ToArrayConverter() { }
public static ToArrayConverter Instance { get; } = new();
}
[MarkupExtensionReturnType(typeof(NullToBoolConverter))]
public class ToArrayExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ToArrayConverter.Instance;
}
}
}
using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Markup;
namespace Converters
{
public class AllEqualsConverter : IMultiValueConverter
{
private AllEqualsConverter() { }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values?.Length > 1)
{
object first = values[0];
return values.Skip(1).All(item => Equals(item, first));
}
return true;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public static AllEqualsConverter Instance { get; } = new AllEqualsConverter();
}
[MarkupExtensionReturnType(typeof(AllEqualsConverter))]
public class AllEqualsExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
=> AllEqualsConverter.Instance;
}
}
using Converters;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
namespace Core2023
{
public static class AttachedProperties
{
public static readonly DependencyProperty AttributeNameProperty
= DependencyProperty.RegisterAttached(
"AttributeName",
typeof(string),
typeof(AttachedProperties),
new PropertyMetadata(string.Empty)
{
CoerceValueCallback = (_, e) => e is null ? string.Empty : e,
PropertyChangedCallback = (d, e) => AllChanged(d, GetArrayOfValues(d) ,(string)e.NewValue)
});
public static string GetAttributeName(DependencyObject obj)
{
return (string)obj.GetValue(AttributeNameProperty);
}
public static void SetAttributeName(DependencyObject obj, string value)
{
obj.SetValue(AttributeNameProperty, value);
}
public static IList<object> GetArrayOfValues(DependencyObject obj)
{
return (IList<object>)obj.GetValue(ArrayOfValuesProperty);
}
public static void SetArrayOfValues(DependencyObject obj, IList<object> value)
{
obj.SetValue(ArrayOfValuesProperty, value);
}
// Using a DependencyProperty as the backing store for ArrayOfValues. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ArrayOfValuesProperty =
DependencyProperty.RegisterAttached(
"ArrayOfValues",
typeof(IList<object>),
typeof(AttachedProperties),
new PropertyMetadata(Array.Empty<object>())
{
CoerceValueCallback = (_, e) => e is null ? Array.Empty<object>() : e,
PropertyChangedCallback = (d, e) => AllChanged(d, (IList<object>)e.NewValue, GetAttributeName(d))
});
private static void AllChanged(DependencyObject d, IList<object> sources, string propertyName)
{
if (sources.Count < 2)
{
d.ClearValue(IsEqualsProperty);
}
else
{
object working = sources[0];
object saved = sources[1];
string propertyPath = GetAttributeName(d);
MultiBinding bindingEquals = new()
{
Converter = AllEqualsConverter.Instance
};
bindingEquals.Bindings.Add(new Binding(propertyPath) { Source = working });
bindingEquals.Bindings.Add(new Binding(propertyPath) { Source = saved });
BindingOperations.SetBinding(d, IsEqualsProperty, bindingEquals);
}
}
public static bool? GetIsEquals(DependencyObject obj)
{
return (bool?)obj.GetValue(IsEqualsProperty);
}
public static void SetIsEquals(DependencyObject obj, bool? value)
{
obj.SetValue(IsEqualsProperty, value);
}
// Using a DependencyProperty as the backing store for IsEquals. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsEqualsProperty =
DependencyProperty.RegisterAttached(
"IsEquals",
typeof(bool?),
typeof(AttachedProperties),
new PropertyMetadata((bool?)null));
}
}
<Setter Property="Margin" Value="15, 10, 0, 5"/>
<Setter Property="core:AttachedProperties.ArrayOfValues">
<Setter.Value>
<MultiBinding Converter="{cnvs:ToArray}">
<Binding Path="WorkingConfig"/>
<Binding Path="SavedConfig"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="core:AttachedProperties.IsEquals"
Value="False">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
P.S. 我马上警告你。我主要在消息编辑器中编写代码(基于我的个人库)。 因此,可能会出现一些小错误。