Search code examples
c#wpfdata-bindinguser-controls

Proper way to create Custom Bindable WPF Control


I want to ask about the right way if I want to create Bindable user control consisting of two controls. I am not sure about what I am doing - whether I do it correctly , because I run into some problems.

Here is what I am trying to do:

Lets call this control ucFlagControl . Create new , custom user control ... Its purpose is to show Color interpretation of logic ( True/ False ) value in variable , type of Bool.

What I used to do before was that I use Rectangle, and Bind FillProperty to boolean value using Converter

What I did to make it works was , that I made a usercontrol , and put rectangle and label inside than I added this code:

 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;
        }
    }

}

} Than I realized that I can easily bind content and I do not have to create public static readonly DependencyProperty LabelContentProperty

but just property

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

this overrides the original content so I am able to Bind and/or assign the text of the label in upper level - in e.g. MainWindow.xaml where this user control is put

First question is if this is in this case OK or if there is some background I am not aware of and I should even such small controls do in different way - I would like to make dll. from it an load it to toolbox - I tested it works. And than use it in for example stack panel .

Second question is that I have problem with a rectangle "Fill" property . I am not able to bind that property like I bind content . I know that the rectangle is derived from Shape class so I am not sure if it has something to do with this.

If I am able to do the inner binding or connection same as in

Content

I can remove the converters than and just bind it in e.g. MainWindow.xaml file (using the converter and converter parameter )

But FillProperty does not work for me so I am not sure about my point of view .

Thank you for suggestions

EDIT:

well I am sorry but I did not catch all you want to say in a comment below. Could you please explain closer ? I know that the code above is not the right way to do it ... ? Or can you post any article about it ? my actual code is like this: In a user control ... I removed all the code from code behind ...

'  <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" />'

Content property works, I cant see Rectangle , and rectangle fill property ... Other problem is if I fill in Content property in XAML where my uc is placed , Rectangle disappears .


Solution

  • I know I'm a year late to the party, but I'll answer incase anyone else comes across this.


    My Suggestions

    1. You should use a TextBlock control instead of Label controls if you want to display pure text. Labels have a content element which is re-rendered/computed many more times than a TextBlock's simple Text property.

    2. You should avoid using magic strings, e.g. "LabelContent". You should use the C# nameof() expression when referencing property names. For example:

    I use lambda expressions to clean up the code a bit, but this is just preference.

    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"));
    
    

    This will prevent runtime errors due to mistyped text, will allow you to jump to the property's reference, will make refactoring easier, and will make debugging easier by giving you a compile error that's easy to find (if the property doesn't exist).

    1. I don't think you need the rectangle. If you're just trying to change the background color of the text area you can use a DataTrigger or make a converter.

    DataTrigger Example

    <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>
    

    A DataTrigger is a quick and easy way to style a control by binding to a property on your ViewModel (assuming you're using the MVVM structure), but there are some cons - like reusing the same style on a different View whose ViewModel's properties are different. You'd have to rewrite the entire styling again.

    Lets turn it into a reusable control where we can (1) specify a highlight background color, and (2) use a boolean to determine whether the control is highlighted.


    Template Controls vs UserControls

    I make my templated controls in a separate C# class file and put the control's styling in another separate resource dictionary file instead of using a UserControl.

    • These templated controls can consist of several other controls to make a single reusable control.
    • It's my understanding that UserControls are meant to use multiple templated controls (e.g. a TextBox) and link their interactions together to perform a specific way.
    • I don't think these controls are meant to be reusable in separate unrelated projects - they display data depending on your ViewModel which can be situational.
    • If you want to extend your custom control in the future via inheritance, then using a UserControl will make things difficult.

    Here's what a few of my controls look like in the solution explorer: Solution Files Snippet

    • The ExpansionPanel control in the snippet is an Expander with additional functionalities/properties.
    • The NavButton is a Button with additional functionalities/properties also.
    • I have a NavigationView UserControl that uses both of those controls to create something much larger than a templated control.

    It sounds like you want to create a reusable templated control.


    Creating a Custom Control

    Here are the basic steps:

    1. Create a "Themes" folder at the root of your project. It must be at the root of your project and spelling does matters.
    2. Create a Generic.xaml Resource Dictionary file in the "Themes" folder. It must be directly under the "Themes" folder and spelling does matters.
    • This is where you store the default themes for your custom controls.
    • The template style for your control will automatically be added to the Generic.xaml file when you add a Custom Control template to your project.
    <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>
    
    • Personally, I like to have separate .xaml file for each control, and then I merge it into the Generic.xaml resource dictionary. This is just for organization purposes.
    <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>
    
    
    • It's important to note that order does matter if you have controls that depend on other controls.
    1. Merge the Generic.xaml file into your App.xaml file.
    <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>
    
    • Why not just merge the control templates in the App.xaml file directly? WPF looks directly for the Generic.xaml file for custom type themes. App.xaml is also application specific and wouldn't be able to be usable in other applications if you used the library as a control library.
    1. Create a .cs file using the built in Custom Control template OR a standard C# class file.

    Your control's .cs file would resemble something similar to...

    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. Create a ResourceDictionary.xaml file to store your control's template and style OR add it directly in Generic.xaml.

    Your .xaml file would look something like...

    <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}" />
    

    Add a reference to the control's resource dictionary in Generic.xaml, like in step 2's code snippet.


    Usage:

    I'm binding the IsChecked property to a IsHighlighted property on my ViewModel. You can bind it to whatever.

    <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

    • Your controls may look a bit different - I'm using a custom dark theme.