Search code examples
c#wpfcustom-controls

WPF Custom Control Usage and Access


Good afternoon everyone. I realize that this subject may be covered in other places.

I am pretty experienced in C# but most of that experience was using console apps, Win Forms not WPF for GUI's, and not MVVM. I am writing a program that detects USB-RS422 devices then provides the user configuration options for each device discovered. The system will have 1->8 of these devices, initially it was 1->4 so I had manually created the layouts for each of them. Now that the number has increased I have made an attempt at creating a custom UserControl but I am struggling with a few concepts.

Here is the code for my custom control: rs422DeviceControlGroup.xaml

<UserControl
x:Class="EngineeringLabUi.rs422DeviceControlGroup"
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:internal="clr-namespace:rs422DeviceControlGroup"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
    <GroupBox
        Grid.Row="1"
        Grid.Column="0"
        Header="Test"
        Style="{StaticResource GroupBoxStyle}">
        <StackPanel x:Name="rs422DevStackPnl" Orientation="Vertical">
            <Border BorderThickness="1">
                <StackPanel Orientation="Horizontal">
                    <CheckBox x:Name="rs422DevEditCkBx" Content="Edit" />
                    <CheckBox x:Name="rs422DevUseCkBx" Content="Use" />
                    <TextBlock Text="Read Type" />
                    <ComboBox x:Name="rs422DevReadTypeComboBox" SelectedIndex="0" />
                    <TextBlock Text="Write Type" />
                    <ComboBox x:Name="rs422DevWriteTypeComboBox" SelectedIndex="0" />
                </StackPanel>
            </Border>
            <Border BorderThickness="1">
                <StackPanel Orientation="Horizontal">
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Baud" />
                            <ComboBox x:Name="rs422DevBoadRatesComboBox" SelectedIndex="0" />
                        </StackPanel>
                    </Border>
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Parity" />
                            <ComboBox x:Name="rs422DevParityComboBox" SelectedIndex="0" />
                        </StackPanel>
                    </Border>
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Data Bits" />
                            <ComboBox x:Name="rs422DevDataBitsComboBox" SelectedIndex="0" />
                        </StackPanel>
                    </Border>
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Stop Bits" />
                            <!--  stop bits value of None is not currently supported  -->
                            <ComboBox x:Name="rs422DevStopBitsComboBox" SelectedIndex="1" />
                        </StackPanel>
                    </Border>
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="FCC Position" />
                            <ComboBox x:Name="rs422DevFccPositionComboBox" SelectedIndex="0" />
                        </StackPanel>
                    </Border>
                    <Border BorderThickness="1">
                        <StackPanel Orientation="Vertical">
                            <TextBlock Text="Status" />
                            <Ellipse x:Name="rs422DevStatusEllipse" />
                            <Button x:Name="rs422DevCheckBtn" Content="Check" />
                        </StackPanel>
                    </Border>
                </StackPanel>
            </Border>
        </StackPanel>
    </GroupBox>
</Grid>

And here is how it presents in design view enter image description here

  1. When defining a custom control like this should I be setting names for the objects in the control such as this? <Button x:Name="rs422DevCheckBtn" Content="Check" />

  2. When my application detects a device I want to add one of these controls to a TabItem but I am unsure how to get down into each of the checkboxes, comboboxes, ellipse and buttons.

I have also created styles in the App.xaml file as shown below:

<Application
x:Class="EngineeringLabUi.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringLabUi"
StartupUri="MainWindow.xaml">

<Application.Resources>
    <!--#region buttonStyle-->
    <Style TargetType="Button">
        <Setter Property="Padding" Value="2" />
        <Setter Property="Margin" Value="1,1,1,1" />
        <Setter Property="Height" Value="auto" />
        <Setter Property="Width" Value="auto" />
        <Setter Property="IsEnabled" Value="False" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
    </Style>
    <!--#endregion buttonStyle-->
    <!--#region borderStyle-->
    <Style TargetType="Border">
        <Setter Property="Padding" Value="2" />
        <Setter Property="Margin" Value="2" />
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="BorderThickness" Value="2" />
        <Setter Property="CornerRadius" Value="6" />
        <Setter Property="Opacity" Value="100" />
    </Style>
    <!--#endregion borderStyle-->
    <!--#region comboBoxStyle-->
    <Style TargetType="ComboBox">
        <Setter Property="Padding" Value="5" />
        <Setter Property="Margin" Value="2" />
        <Setter Property="Height" Value="auto" />
        <Setter Property="Width" Value="auto" />
        <!--< debug />-->
        <!--<Setter Property="IsEnabled" Value="False" />-->
        <Setter Property="IsEnabled" Value="True" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
    </Style>
    <!--#endregion comboBoxStyle-->
    <!--#region labelStyle-->
    <Style TargetType="Label">
        <Setter Property="Padding" Value="1" />
        <Setter Property="Margin" Value="1,1,1,1" />
        <Setter Property="Height" Value="auto" />
        <Setter Property="Width" Value="auto" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="FontWeight" Value="Bold" />
    </Style>
    <!--#endregion labelStyle-->
    <!--#region textBlockStyle-->
    <Style TargetType="TextBlock">
        <Setter Property="Padding" Value="1" />
        <Setter Property="Margin" Value="3,3,3,3" />
        <Setter Property="Height" Value="auto" />
        <Setter Property="Width" Value="auto" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="TextDecorations" Value="Underline" />
        <!--<Setter Property="bo" Value="Underline" />-->
    </Style>
    <!--#endregion textBlockStyle-->
    <!--#region groupBoxStyle-->
    <Style x:Key="GroupBoxStyle" TargetType="{x:Type GroupBox}">
        <Setter Property="BorderBrush" Value="#D5DFE5" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type GroupBox}">
                    <Grid SnapsToDevicePixels="true">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="auto" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="auto" />
                            <ColumnDefinition Width="auto" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="auto" />
                            <RowDefinition Height="auto" />
                        </Grid.RowDefinitions>
                        <Border
                            x:Name="Header"
                            Grid.Row="0"
                            Grid.Column="1"
                            Padding="3,1,3,0">
                            <ContentPresenter
                                ContentSource="Header"
                                RecognizesAccessKey="True"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </Border>
                        <ContentPresenter
                            Grid.Row="2"
                            Grid.Column="1"
                            Grid.ColumnSpan="2"
                            Margin="{TemplateBinding Padding}"
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!--#endregion groupBoxStyle-->
    <!--#region checkBoxStyle-->
    <Style TargetType="CheckBox">
        <Setter Property="Padding" Value="0" />
        <Setter Property="Margin" Value="10,1,1,1" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <!--< debug />-->
        <!--<Setter Property="IsEnabled" Value="False" />-->
        <Setter Property="IsEnabled" Value="True" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
    </Style>
    <!--#endregion checkBoxStyle-->
    <!--#region ellipseStyle-->
    <Style TargetType="Ellipse">
        <Setter Property="Height" Value="15" />
        <Setter Property="Margin" Value="1,1,1,1" />
        <Setter Property="StrokeThickness" Value="3" />
        <Setter Property="Width" Value="15" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="IsEnabled" Value="False" />
        <Setter Property="Fill" Value="Gray" />
        <Setter Property="Stroke" Value="Black" />
    </Style>
    <!--#endregion ellipseStyle-->
    <!--#region tabControltyle-->
    <Style TargetType="TabControl">
        <Setter Property="Padding" Value="0" />
        <Setter Property="Margin" Value="1,1,1,1" />
        <Setter Property="FontSize" Value="10" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Stretch" />
    </Style>
    <!--#endregion tabControltyle-->
    <!--#region tabItemStyle-->
    <Style TargetType="TabItem">
        <Setter Property="Padding" Value="3.5" />
        <Setter Property="Margin" Value="-2,0,-2,0" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="Height" Value="22" />
    </Style>
    <!--#endregion tabItemStyle-->
    <!--#region menuItemStyle-->
    <Style TargetType="MenuItem">
        <!--< debug />-->
        <!--<Setter Property="Foreground" Value="#FF020202" />-->
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="Height" Value="22" />
    </Style>
    <!--#endregion menuItemStyle-->
    <!--#region flowDocumentStyle-->
    <Style TargetType="FlowDocument">
        <Setter Property="ColumnRuleBrush" Value="Black" />
        <Setter Property="ColumnRuleWidth" Value="1" />
        <Setter Property="IsColumnWidthFlexible" Value="True" />
        <Setter Property="MaxPageWidth" Value="1000" />
        <Setter Property="PageWidth" Value="auto" />
        <Setter Property="MaxPageHeight" Value="700" />
    </Style>
    <!--#endregion flowDocumentStyle-->
    <!--#region gridStyle-->
    <Style TargetType="Grid">
        <Setter Property="MaxHeight" Value="700" />
        <Setter Property="MaxWidth" Value="1400" />
        <Setter Property="Height" Value="auto" />
        <Setter Property="Width" Value="auto" />
        <Setter Property="Background" Value="#FFE5E5E5" />
    </Style>
    <!--#endregion gridStyle-->
    <!--#region richTextBoxStyle-->
    <Style TargetType="RichTextBox">
        <Setter Property="MinHeight" Value="500" />
        <Setter Property="MaxHeight" Value="700" />
        <Setter Property="MinWidth" Value="500" />
        <Setter Property="MaxWidth" Value="1000" />
        <Setter Property="HorizontalScrollBarVisibility" Value="auto" />
        <Setter Property="VerticalScrollBarVisibility" Value="auto" />
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
        <Setter Property="VerticalContentAlignment" Value="Stretch" />
    </Style>
    <!--#endregion richTextBoxStyle-->
</Application.Resources>

In my MainWindow.xaml I am able to use the control as shown below but like I said I am unsure how to set properties of the objects within the custom control. Also, this is my first time making a custom control so I also wonder if I haven't done it correctly or if I should do something different. My application is not currently MVVM but I do intend to migrate to that architecture in the near future.

    <!-- this is just a test control to organize the layout I intend to use elsewhere-->
<TabControl Margin="254,127,53,273">
    <TabItem Header="COM 4">
        <Grid Background="#FFE5E5E5">
            <local:rs422DeviceControlGroup x:Name="SomeRs422Device" >
                <!-- I'm not sure what I should do here to access the parts of the control-->
            </local:rs422DeviceControlGroup>
        </Grid>
    </TabItem>
    <TabItem Header="COM 5">
        <local:rs422DeviceControlGroup />
    </TabItem>
</TabControl>

I suppose one approach would be to just have a TabControl with TabItems enough for 8 devices and then in the code behind do the instantiation there during runtime.

I'm looking for some advice on this as changes after this point in the application will require allot of rework and I would like to ensure that my approach is concrete before moving forward. MVVM is somewhat new to me and so is WPF. I appologise if this post is duplicating other post content. I have been researching for about a week and need to move forward.

Thanks in advance!


Solution

  • Question number 1:

    You need names when you want to reference the control in code behind(which is the last thing you want to do when you are implementing MVVM pattern) or when you are using bindings where simply you bind a value to your control in View from View Model property. If your contol is for example of type textbox and you have for example text property you will bind the value to your control in that View from the corresponding View model. Read about binding values to a certain control property which is exacly you want to do.

    Question number 2:

    If you want your solution in Model View View Model pattern fashion you need to learn about separation of concern in WPF.

    Furthermore, simply as I said if you have a control(checkbox, combobox etc) you can bind a value to that control in that View from corresponding View Model. In short, you need to define everything in View Model related to your View(properties which you bind to controls values, commands for buttons if you have any etc and off course to implement INotify property changed inteface in order to have full real time working solution). So, you get it, you can make a View from your User Control and define everything for each control in View model and then you will have with every value you need and that is in short. I would like to mention that you need to check data templates which is a very powerfull thing when you want to define a something like this in WPF.

    Finally, I would make a resource dictionary from everything you defined in App.xaml and reference it in the same file in merged dictionary and that is it.

    So, in a case that you want a user control inside your WPF tab contols you would something like this:

    <TabControl Name="myTabControl">
        <TabItem Header="Tab 1">
            <local:YourUserControlName />
        </TabItem>
        <TabItem Header="Tab 2">
            <local:YourUserControlName />
        </TabItem>
        <!-- Add more TabItems as needed -->
    </TabControl>
    

    where local is the namespace where you defined your user conrol.

    In case that you want to use a View inside a tab control in WPF(because you want to define everything in View Model - properties for comboboxes, checkboxes etc.) you would do something like this:

    <TabControl Name="myTabControl">
        <TabItem Header="Tab 1">
            <ContentControl>
                <ContentControl.Content>
                    <local:YourViewName />
                </ContentControl.Content>
            </ContentControl>
        </TabItem>
        <TabItem Header="Tab 2">
            <ContentControl>
                <ContentControl.Content>
                    <local:YourViewName />
                </ContentControl.Content>
            </ContentControl>
        </TabItem>
        <!-- Add more TabItems as needed -->
    </TabControl>
    

    Content Control will host your View. Finally, one of my favorite is to make a Data Template like this:

    <DataTemplate x:Key="MyTabItemContentTemplate">
        <!-- Your UI elements and data bindings go here -->
    </DataTemplate>
    

    Where this Data Temlate can be in a resource dictionary for example but you need to reference it in Main.

    <TabControl Name="myTabControl">
        <TabItem Header="Tab 1" ContentTemplate="{StaticResource MyTabItemContentTemplate}">
            <!-- Data context for Tab 1 content will be bound to the DataTemplate -->
        </TabItem>
        <TabItem Header="Tab 2" ContentTemplate="{StaticResource MyTabItemContentTemplate}">
            <!-- Data context for Tab 2 content will be bound to the DataTemplate -->
        </TabItem>
        <!-- Add more TabItems as needed -->
    </TabControl>
    

    In this case, the content template property of each tab item is defined with Data Template(again with all your custom controls from a user control) and you need to set a data context in order to bind values and everything you need for those controls.

    Finally, keywords are View, View Model, Notify property change interface, Data Context, bindings and that is it.