简单的属性网格控件
一个 .NET XAML 用户控件,用于获取一个简单的 PropertyGrid
引言
这段代码帮助(我希望如此)所有 WPF/XAML 初学者找到一个简单的属性网格,几乎可以在他们的代码中使用,无需或只需少量努力,并且没有额外的依赖项。
我曾多次被要求用“PropertyGrid
”类型的控件替换应用程序的选项页面,但由于缺乏时间和基本的 .NET Framework 元素一直阻止我这样做。 然而,最终,这项任务再也不能推迟了,我不得不发明(……)一些具有以下特征的东西
- 它必须显示和编辑字段的值。 编辑必须尽可能地用户友好,这意味着布尔值必须用
CheckBox
选择,Enum
用ComboBox
选择。 数值和string
可以用TextBox
。 - 我不想重写大量的视图模型,甚至不想重写同一个控件。
- 我拥有的依赖项越少越好。
- 如果我必须在另一个软件中再次使用它,我必须能够在不修改原始代码或在另一个应用程序中使用现有样式的情况下对其进行样式设置。
- 另一个新手有一天可以使用/修改它,所以它必须非常简单。
了解了这些,让我们开始吧。
背景
作为我自己也是一个初学者,几乎没有要求,但是对 XAML/WPF 的一点了解会有所帮助。
Using the Code
首先,让我们编写用户控件。
<!--PropertyGridUC.xaml-->
<UserControl
x:Class="IssamTp.Lib.Wpf.PropertyGridUC"
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:local="clr-namespace:IssamTp.Lib.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<UserControl.Resources>
<DataTemplate x:Key="EnumDataTemplate"
DataType="{x:Type local:PropertyGridRowVM}">
<ComboBox ItemsSource="{Binding Path=SelectableValues}"
SelectedItem="{Binding Value, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Style="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC},
Path=ComboBoxEditingStyle}" />
</DataTemplate>
<DataTemplate x:Key="BoolDataTemplate">
<CheckBox IsChecked="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"
Style="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}, Path=CheckBoxEditingStyle}" />
</DataTemplate>
<DataTemplate x:Key="IntegralDataTemplate">
<TextBox Text="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"
Style="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC},
Path=TextBoxEditingStyle}" />
</UserControl.Resources>
<DataGrid
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Path=PropertiesValues}"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Path=Property, Mode=OneWay}"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}}"
Text="{Binding Path=HeaderLabelProperty}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<TextBlock DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:PropertyGridUC}}"
Text="{Binding Path=HeaderLabelValue}" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplateSelector>
<local:TypeSelector
BoolDataTemplate=
"{StaticResource ResourceKey=BoolDataTemplate}"
EnumDataTemplate="{StaticResource ResourceKey=EnumDataTemplate}"
IntegralDataTemplate="{StaticResource
ResourceKey=IntegralDataTemplate}" />
</DataGridTemplateColumn.CellTemplateSelector>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>
正如你所看到的,除了我们稍后将看到的value bindings之外,XAML 中没有什么特别的东西。
该控件包含一个简单的 DataGrid
,其中有两列,一列具有与属性名称(或其描述)绑定的不可编辑值,另一列具有它的值(同样,我们稍后会看到这些绑定来自哪里)。
DataTemplates
的标题和样式与 Dependency Properties 绑定,以便最终用户可以自定义它们。
xaml.cs 文件是这样的
using System.Windows;
using System.Windows.Controls;
namespace IssamTp.Lib.Wpf
{
public partial class PropertyGridUC : UserControl
{
#region Dependency Properties
#region HeaderLabelProperty
public string HeaderLabelProperty
{
get { return (string)GetValue(HeaderLabelPropertyProperty); }
set { SetValue(HeaderLabelPropertyProperty, value); }
}
public static readonly DependencyProperty HeaderLabelPropertyProperty =
DependencyProperty.Register("HeaderLabelProperty", typeof(string),
typeof(PropertyGridUC), new PropertyMetadata("Property"));
#endregion
#region HeaderLabelValue
public string HeaderLabelValue
{
get { return (string)GetValue(HeaderLabelValueProperty); }
set { SetValue(HeaderLabelValueProperty, value); }
}
public static readonly DependencyProperty HeaderLabelValueProperty =
DependencyProperty.Register("HeaderLabelValue", typeof(string),
typeof(PropertyGridUC), new PropertyMetadata("Value"));
#endregion
#region ComboBoxEditingStyle
public Style ComboBoxEditingStyle
{
get { return (Style)GetValue(ComboBoxEditingStyleProperty); }
set { SetValue(ComboBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty ComboBoxEditingStyleProperty =
DependencyProperty.Register("ComboBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(ComboBox))));
#endregion
#region CheckBoxEditingStyle
public Style CheckBoxEditingStyle
{
get { return (Style)GetValue(CheckBoxEditingStyleProperty); }
set { SetValue(CheckBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty CheckBoxEditingStyleProperty =
DependencyProperty.Register("CheckBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(CheckBox))));
#endregion
#region TextBoxEditingStyle
public Style TextBoxEditingStyle
{
get { return (Style)GetValue(TextBoxEditingStyleProperty); }
set { SetValue(TextBoxEditingStyleProperty, value); }
}
public static readonly DependencyProperty TextBoxEditingStyleProperty =
DependencyProperty.Register("TextBoxEditingStyle", typeof(Style),
typeof(PropertyGridUC), new PropertyMetadata(new Style(typeof(TextBox))));
#endregion
#endregion
#region Ctor
public PropertyGridUC()
{
InitializeComponent();
}
#endregion
}
}
对于像我这样的新手来说,选择正确的编辑控件可能是整个代码中最难的部分:我们必须指定一个 CellTemplateSelector
,这可以通过以下简单代码完成。
using System;
using System.Windows.Controls;
using System.Windows;
namespace IssamTp.Lib.Wpf
{
/// <summary>Permette di scegliere tra tre DataTemplate distinguendo tra bool,
/// Enum e tutti gli altri.</summary>
public class TypeSelector : DataTemplateSelector
{
#region Proprietà
public DataTemplate? BoolDataTemplate
{
get;
set;
}
public DataTemplate? EnumDataTemplate
{
get;
set;
}
public DataTemplate? IntegralDataTemplate
{
get;
set;
}
#endregion
public override DataTemplate SelectTemplate
(object item, DependencyObject container)
{
if (item is PropertyGridRowVM rowVM)
{
if (rowVM.Value is Enum && EnumDataTemplate != null)
{
return EnumDataTemplate;
}
else if (rowVM.Value is bool && BoolDataTemplate != null)
{
return BoolDataTemplate;
}
else if (IntegralDataTemplate != null)
{
return IntegralDataTemplate;
}
else
{
throw new MemberAccessException("No data template set.");
}
}
else if (IntegralDataTemplate != null)
{
return IntegralDataTemplate;
}
else
{
throw new MemberAccessException("No data template set.");
}
}
}
}
看到了吗? 我们只需声明你需要的所有 DataTemplate
属性(在本例中为三个),并覆盖 SelectTemplate
方法来选择你想要的那个。
现在,我们必须用一些数据来填充这个控件,这意味着我们将看到绑定的来源。 这是“概念性”部分,但同样没什么特别的。
记住“保持简单”和“不要浪费你已经拥有的”的要求,我编写了这两个类
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace IssamTp.Lib.Wpf
{
public interface IPropertyGridVM
{
ObservableCollection<PropertyGridRowVM> PropertiesValues
{
get;
}
}
public class PropertyGridRowVM : INotifyPropertyChanged
{
public object? Value
{
get => _Value;
set
{
if (_Value == null || !_Value.Equals(value))
{
_Valore = value;
NotifyPropertyChanged();
}
}
}
private object? _Value;
public string Property
{
get => _Property;
set
{
if (!_Property.Equals(value, StringComparison.Ordinal))
{
_Property = value;
NotifyPropertyChanged();
}
}
}
private string _Property = string.Empty;
public ObservableCollection<object?> SelectableValues
{
get;
private set;
} = new ObservableCollection<object?>();
public Type? PropertyType
{
get;
private set;
}
#region INotifyPropertyChanged
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Ctors
internal PropertyGridRowVM()
: base()
{
}
public PropertyGridRowVM(string propertyName, object value)
: base()
{
Value = value;
Property = propertyName;
PropertyType = Valore.GetType();
if (Value is Enum)
{
foreach (object? enumValue in PropertyType.GetEnumValues())
{
SelectableValues.Add(enumValue);
}
}
}
#endregion
}
}
为什么是一个接口? 好吧,我们在 C# 中没有多重继承,所以这是我找到的一种方法,使控件可以与我所有现有的视图模型一起使用,而 PropertyGridRowVM
使绑定的魔力成为可能(我最初的想法是使用命名元组,你可以在这里找到我为什么切换到这个解决方案)。
就是这样! 我们现在只需要一些代码来测试它
<Window x:Class="Test.Wpf.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:Test.Wpf"
xmlns:issam="clr-namespace:IssamTp.Lib.Wpf;assembly=IssamTp.Lib.Wpf"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="{x:Type ComboBox}" x:Key="ComboBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style TargetType="{x:Type CheckBox}" x:Key="CheckBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style TargetType="{x:Type TextBox}" x:Key="TextBoxStyle">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
</Style>
</Window.Resources>
<Grid>
<issam:PropertyGridUC HeaderLabelProperty="Props" HeaderLabelValue="Vals"
DataContext="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType=local:MainWindow}}"
ComboBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=ComboBoxStyle}}"
CheckBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=ComboBoxStyle}}"
TextBoxEditingStyle="{Binding Source=
{StaticResource ResourceKey=TextBoxStyle}}"/>
</Grid>
</Window>
using IssamTp.Lib.Wpf;
using System.Collections.ObjectModel;
using System.Windows;
namespace Test.Wpf
{
public partial class MainWindow : Window, IPropertyGridVM
{
enum GetSome
{
One,
Two,
Three,
};
public ObservableCollection<PropertyGridRowVM> PropVals
{
get;
private set;
} = new ObservableCollection<PropertyGridRowVM>();
public MainWindow()
{
InitializeComponent();
PropVals.Add(new PropertyGridRowVM("A text value", "Hello world!"));
PropVals.Add(new PropertyGridRowVM("Count!", GetSome.Three));
PropVals.Add(new PropertyGridRowVM("Is it true?", true));
}
}
}
正如你所看到的,我所要做的就是实现我的接口并添加一些数据。 在这里,我使用了代码作为视图模型,但这并不重要,你可以将其添加到任何地方。
结论
该代码很简单,可能有人做得更好,但在使用其他人的解决方案多年后,我想回馈给宇宙一些(有用的)东西。
如果你需要,请随意使用它,改进它,并告诉我我是否可以做得更好。你可以从我的 git 仓库中看到,我用意大利语编写我的代码,所以我可能遗漏或拼错了某些变量/方法名称,如果你在文章中发现一些错误,请告诉我。