创建自定义可绑定 WPF 控件的正确方法

Proper way to create Custom Bindable WPF Control

我想问一下如果我想创建由两个控件组成的可绑定用户控件的正确方法。我不确定自己在做什么——是否正确,因为我 运行 遇到了一些问题。

这是我正在尝试做的事情:

让我们称这个控件为 ucFlagControl 。创建新的自定义用户控件... 其目的是显示布尔类型变量中逻辑(真/假)值的颜色解释。

我以前做的是使用Rectangle,然后使用Converter

FillProperty绑定到boolean值

我为让它工作所做的是,我做了一个用户控件,并在里面放了矩形和标签 比我添加此代码:

 public partial class ucStatusFlag : UserControl
{
    public ucStatusFlag()
    {
        InitializeComponent();


    }

    public string LabelContent
    {
        get { return (string)GetValue(LabelContentProperty); }
        set
        {
            SetValue(LabelContentProperty, value);
            OnPropertyChanged("LabelContent");
        }
    }


    ///in case that I use integer or array
    public int BitIndex
    {
        get { return (int)GetValue(BitIndexProperty); }
        set
        {
            SetValue(BitIndexProperty, value);
            OnPropertyChanged("BitIndex");
        }
    }

    public string BindingSource
    {
        get { return (string)GetValue(BindingSourceProperty); }
        set
        {
            SetValue(BindingSourceProperty, value);
            OnPropertyChanged("BindingSource");
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }


    /// <summary>
    /// Identified the Label dependency property
    /// </summary>
    public static readonly DependencyProperty LabelContentProperty =
        DependencyProperty.Register("LabelContent", typeof(string), typeof(ucStatusFlag), new PropertyMetadata("LabelContent"));

    public static readonly DependencyProperty BitIndexProperty =
        DependencyProperty.Register("BitIndex", typeof(int), typeof(ucStatusFlag), new PropertyMetadata(0));

    public static readonly DependencyProperty BindingSourceProperty =
        DependencyProperty.Register("(BindingSource", typeof(string), typeof(ucStatusFlag), new PropertyMetadata(""));


    private void StatusFlag_Loaded(object sender, RoutedEventArgs e)
    {
        if (BindingSource.Length > 0)
        {
            Binding bind = new Binding();
            string s = LabelContent;
            int i = BitIndex;


             bind.Converter = new StatusToColor();





            bind.Path = new PropertyPath(BindingSource);
            bind.ConverterParameter = BitIndex.ToString();
            bind.Mode = BindingMode.OneWay;
            bind.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            recStatusBit.SetBinding(Rectangle.FillProperty, bind);
        }
    }

    private class StatusToColor : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {

            byte bDataWordIdx;
            byte bDataBitIdx;

            Byte.TryParse((string)parameter, out bDataBitIdx);

            if (Object.ReferenceEquals(typeof(UInt16[]), value.GetType()))
            {
                UInt16[] uiaData = (UInt16[])value;
                bDataWordIdx = (byte)uiaData[0];


                if ((uiaData[bDataBitIdx / 16] >> (bDataBitIdx % 16) & 0x1) == 1)
                {
                    return Brushes.Green;
                }
                else
                {
                    return Brushes.Red;
                }
            }
            else if (Object.ReferenceEquals(typeof(UInt16), value.GetType()))
            {
                UInt16 uiaData = (UInt16)value;

                if (((uiaData >> bDataBitIdx) & 0x1) == 1)
                {
                    return Brushes.Green;
                }
                else
                {
                    return Brushes.Red;
                }
            }
            return 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return 0;
        }
    }

}

} 比我意识到我可以轻松绑定内容而且我不必创建 public static readonly DependencyProperty LabelContentProperty

但只是 属性

 public new string Content
    {
        get { return (string)label.Content; }
        set
        {
            SetValue(label.Content, value);
            OnPropertyChanged("Content");
        }
    }

这会覆盖原始内容,因此我可以绑定 and/or 分配上层标签的文本 - 例如MainWindow.xaml 放置此用户控件的位置

第一个问题是在这种情况下是否可以,或者是否有一些我不知道的背景,我什至应该以不同的方式进行如此小的控制 - 我会喜欢制作dll。从它加载到工具箱 - 我测试了它的工作原理。而不是在例如堆栈面板中使用它。

第二个问题 是我对矩形 "Fill" 属性 有问题。我无法像这样绑定 属性我绑定内容。 我知道矩形是从 Shape class 派生的,所以我不确定它是否与此有关。

如果我能够像

一样进行内部绑定或连接
Content

我可以删除转换器,然后将其绑定到例如MainWindow.xaml 文件(使用转换器和转换器参数)

但是 FillProperty 对我不起作用,所以我不确定我的观点。

感谢您的建议

编辑:

很抱歉,我没有听清您在下面的评论中想说的所有内容。你能详细解释一下吗? 我知道上面的代码不是正确的方法......? 或者你能 post 任何关于它的文章吗? 我的实际代码是这样的: 在用户控件中...我从后面的代码中删除了所有代码...

'  <Label x:Name="lStatusBit"  Grid.Column="1" Padding="0" VerticalContentAlignment="Center" Margin="2,1,17,2"  />
        <Rectangle x:Name="recStatusBit"  Margin="0,3,1,7" />'

内容 属性 有效,我看不到 Rectangle 和矩形填充 属性 ... 另一个问题是,如果我在放置我的 uc 的 XAML 中填写内容 属性,矩形就会消失。

我终于发现这个工作正常了:

XAML

<UserControl x:Class="ucStatusFlag"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="17" d:DesignWidth="100" 
         x:Name="StatusFlag">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Rectangle x:Name="recStatusBit" Grid.Column="0" Stroke="Black" Width="11"  Fill="{Binding ElementName=StatusFlag, Path=RectangleColor}" Margin="0,2,0.2,3.8"  />
    <Label  Height="17" x:Name="lStatusBit" Foreground="Black" Grid.Column="1" Padding="0" VerticalContentAlignment="Center" Margin="5,0,0,1" Content="{Binding ElementName=StatusFlag, Path=LabelContent}" />
</Grid>

C#

 public partial class ucStatusFlag : UserControl
{
    public ucStatusFlag()
    {
        InitializeComponent();


    }

    public string LabelContent
    {
        get { return (string)GetValue(LabelContentProperty); }
        set
        {
            SetValue(LabelContentProperty, value);

        }
    }



    public SolidColorBrush RectangleColor
    {
        get { return (SolidColorBrush)GetValue(RectangleColorProperty); }
        set
        {
            SetValue(RectangleColorProperty, value);

        }
    }

    public static readonly DependencyProperty RectangleColorProperty =
        DependencyProperty.Register("RectangleColor", typeof(SolidColorBrush), typeof(ucStatusFlag), new PropertyMetadata(Brushes.Gold));



    public static readonly DependencyProperty LabelContentProperty =
         DependencyProperty.Register("LabelContent", typeof(string), typeof(ucStatusFlag), new PropertyMetadata("LabelContent"));

}

在另一个项目中绑定:

   <ucStatusFlag HorizontalAlignment="Left" Height="18" Margin="154,224,0,0" VerticalAlignment="Top" Width="100" LabelContent="ABC" RectangleColor="{Binding RectangleColorPropertyInProject}"/>

其中 RectangleColor属性InProject 是 属性 在某些项目视图模型中

我知道我迟到了一年,但我会回答,以防其他人遇到这个问题。


我的建议

  1. 如果您想显示纯文本,您应该使用 TextBlock 控件而不是 Label 控件。标签的内容元素 re-rendered/computed 比 TextBlock 的简单 Text 属性.

    多很多倍
  2. 您应该避免使用魔术字符串,例如"LabelContent"。引用 属性 名称时应使用 C# nameof() 表达式。例如:

我使用 lambda 表达式稍微清理一下代码,但这只是偏好。

public string LabelContent
{
    get => (string)GetValue(LabelContentProperty);
    set => SetValue(LabelContentProperty, value);
}

public static readonly DependencyProperty LabelContentProperty =
         DependencyProperty.Register(
              nameof(LabelContent),
              typeof(string), 
              typeof(ucStatusFlag), 
              new PropertyMetadata("Default Value"));

这将防止由于输入错误的文本而导致的运行时错误,将允许您跳转到 属性 的参考,将使重构更容易,并通过给您一个易于解决的编译错误来使调试更容易查找(如果 属性 不存在)。

  1. 我认为您不需要矩形。如果您只是想更改文本区域的背景颜色,您可以使用 DataTrigger 或制作一个转换器。

数据触发器示例

<TextBlock>
    <TextBlock.Style>
        <Style TargetType="{x:Type TextBlock}">
            <!-- The default value -->
            <Setter Property="Background" Value="Transparent" />
                <!-- Your trigger -->
            <Style.Triggers>
                <DataTrigger Binding="{Binding SomeBooleanValue}" Value="True">
                    <Setter Property="Background" Value="Red" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </TextBlock.Style>
</TextBlock>

DataTrigger 是一种通过绑定到 ViewModel 上的 属性 来设置控件样式的快速简便的方法(假设您使用的是 MVVM 结构),但也有一些缺点 - 比如重复使用相同的控件在 ViewModel 的属性不同的不同视图上的样式。您必须再次重写整个样式。

让我们把它变成一个可重用的控件,我们可以 (1) 指定高亮背景颜色,(2) 使用布尔值来确定控件是否高亮。


模板控件与用户控件

我在单独的 C# class 文件中创建了 模板化控件,并将控件的样式放在另一个单独的资源字典文件中,而不是使用 UserControl。

  • 这些模板化控件可以由多个其他控件组成,从而使单个 可重用 控件。
  • 据我了解,UserControls 旨在使用多个模板化控件(例如 TextBox)和 link 它们的交互以执行特定方式。
  • 我不认为这些控件可以在单独的不相关项目中重复使用 - 它们根据您的 ViewModel 显示数据,这可能是情境性的。
  • 如果您以后想通过继承扩展您的自定义控件,那么使用 UserControl 会使事情变得困难。

下面是我的一些控件在解决方案资源管理器中的样子: Solution Files Snippet

  • 代码段中的 ExpansionPanel 控件是一个带有附加 functionalities/properties.
  • 的扩展器
  • NavButton 是一个带有附加 functionalities/properties 的按钮。
  • 我有一个 NavigationView UserControl,它使用这两个控件来创建比模板化控件大得多的东西。

听起来您想创建一个可重用模板化控件。


创建自定义控件

基本步骤如下:

  1. 在项目的根目录下创建一个“Themes”文件夹。它必须位于项目的根目录,并且拼写 很重要。
  2. 在“主题”文件夹中创建一个Generic.xaml 资源字典文件。它必须直接在“主题”文件夹下并且拼写确实很重要。
  • 这是您存储自定义控件的默认主题的地方。
  • 将自定义控件模板添加到项目时,控件的模板样式将自动添加到 Generic.xaml 文件。
<Style TargetType="{x:Type local:Example}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:Example}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
  • 就个人而言,我喜欢为每个控件创建单独的 .xaml 文件,然后将其合并到 Generic.xaml 资源字典中。这仅用于组织目的。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ResourceDictionary.MergedDictionaries>

        <!-- Control template styles -->

        <ResourceDictionary Source="pack://application:,,,/Themes/ExpansionPanel.xaml" />
        <ResourceDictionary Source="pack://application:,,,/Themes/NavButton.xaml" />
        <ResourceDictionary Source="pack://application:,,,/Themes/TextDocument.xaml" />
        <ResourceDictionary Source="pack://application:,,,/Themes/TextDocumentToolBar.xaml" />
        <ResourceDictionary Source="pack://application:,,,/Themes/TextEditor.xaml" />
        <ResourceDictionary Source="pack://application:,,,/Themes/HighlightTextBlock.xaml" />       

        <!-- etc... -->

    </ResourceDictionary.MergedDictionaries>    

    <!-- Other styles or whatever -->

</ResourceDictionary>

  • 请务必注意,如果您的控件依赖于其他控件,那么顺序 确实 很重要。
  1. 将 Generic.xaml 文件合并到您的 App.xaml 文件中。
<Application>
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>

                <!-- Other resource dictionaries... -->

                <ResourceDictionary Source="pack://application:,,,/Themes/Generic.xaml" />              
            </ResourceDictionary.MergedDictionaries>

            <!-- Other resource dictionaries... -->

        </ResourceDictionary>
    </Application.Resources>
</Application>
  • 为什么不直接合并 App.xaml 文件中的控件模板呢? WPF 直接查找自定义类型主题的 Generic.xaml 文件。 App.xaml 也是特定于应用程序的,如果您将该库用作控件库,则无法在其他应用程序中使用。
  1. 使用内置自定义控件模板或标准 C# class 文件创建 .cs 文件。

您的控件的 .cs 文件类似于...

public class HighlightTextBlock : Control
{
    #region Private Properties

    //  The default brush color to resort back to
    public Brush DefaultBackground;

    #endregion

    static HighlightTextBlock()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(HighlightTextBlock), new FrameworkPropertyMetadata(typeof(HighlightTextBlock)));
    }

    // Get the default background color and set it.
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        DefaultBackground = Background;
    }


    #region Dependency Properties

    /// <summary>
    /// The text to display.
    /// </summary>
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        nameof(Text), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(string.Empty));

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    /// <summary>
    /// Whether or not the background should be highlighted.
    /// </summary>
    //  This uses a callback to update the background color whenever the value changes
    public static readonly DependencyProperty HighlightProperty = DependencyProperty.Register(
        nameof(Highlight), typeof(bool),
        typeof(HighlightTextBlock), new PropertyMetadata(false, HighlightPropertyChangedCallback));

    public bool Highlight
    {
        get => (bool)GetValue(HighlightProperty);
        set => SetValue(HighlightProperty, value);
    }

    /// <summary>
    /// The highlight background color when <see cref="Highlight"/> is true.
    /// </summary>
    public static readonly DependencyProperty HighlightColorProperty = DependencyProperty.Register(
        nameof(HighlightColor), typeof(Brush),
        typeof(HighlightTextBlock), new PropertyMetadata(null));

    public Brush HighlightColor
    {
        get => (Brush)GetValue(HighlightColorProperty);
        set => SetValue(HighlightColorProperty, value);
    }

    #endregion

    #region Callbacks

    //  This is the callback that will update the background
    private static void HighlightPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var target = (HighlightTextBlock)dependencyObject;

        if (target.Highlight)
            target.Background = target.HighlightColor;
        else
            target.Background = target.DefaultBackground;
    }

    #endregion

}

  1. 创建一个 ResourceDictionary.xaml 文件来存储控件的模板和样式,或者直接将其添加到 Generic.xaml。

您的 .xaml 文件看起来像...

<Style x:Key="HighlightTextBlock" TargetType="{x:Type ctrl:HighlightTextBlock}">
    <!-- Default setters... -->

    <!-- Define your control's design template -->
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ctrl:HighlightTextBlock}">

                <Border Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}">
                        
                    <!-- 
                        I only bound the Text and Background property in this example
                        Make sure to bind other properties too.. like Visibility, IsEnabled, etc.. 
                    -->
                    <TextBlock Text="{TemplateBinding Text}" />
                </Border>
                    
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- 
    Set the default style for the control 
    The above style has a key, so controls won't use that style
    unless the style is explicitly set. 
    e.g. 
        <ctrl:HighlightTextBlock Style={StaticResource HighlightTextBlock} />
    
    The reason I used a key above is to allow extending/reusing that default style.
    If a key wasn't present then you wouldn't be able to reference it in 
    another style.
-->
<Style TargetType="{x:Type ctrl:HighlightTextBlock}" BasedOn="{StaticResource HighlightTextBlock}" />

在 Generic.xaml 中添加对控件资源字典的引用,就像在步骤 2 的代码片段中一样。


用法:

我正在将 IsChecked 属性 绑定到我的 ViewModel 上的 IsHighlighted 属性。 你可以把它绑定到任何东西。

<StackPanel>
    <ToggleButton IsChecked="{Binding IsHighlighted}" Content="{Binding IsHighlighted}" 
                  Width="100" Height="35" Margin="5"/>

    <ctrl:HighlightTextBlock Background="Transparent" HighlightColor="Red" 
                  Text="HELLO WORLD!!!" Highlight="{Binding IsHighlighted}" 
                  Width="100" Height="35" HorizontalAlignment="Center" />
</StackPanel>

On False Snippet

On True Snippet

  • 您的控件可能看起来有点不同 - 我使用的是自定义深色主题。