Home > database >  How to Populate a User Control with a Reusable User Control
How to Populate a User Control with a Reusable User Control

Time:10-05

I asked this question (enter image description here

I have created this User Control, DeviceInfoRow.xaml: enter image description here

And here is the XAML:

<UserControl 
    x:Class="StagingApp.Main.Controls.Common.DeviceInfoRow"
    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" 
    xmlns:common="clr-namespace:StagingApp.Presentation.ViewModels.Common;assembly=StagingApp.Presentation"
    d:DataContext="{d:DesignInstance Type=common:DeviceInfoRowViewModel}"
    mc:Ignorable="d" >

    <StackPanel
        Style="{StaticResource InfoRowStackPanelStyle}">

        <Label
            Style="{StaticResource DeviceInfoPropertyLabelStyle}"
            x:Name="InfoLabel" />
        <TextBox
            Style="{StaticResource DeviceInfoTextBoxStyle}"
            x:Name="InfoTextBox" />
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <StackPanel
                Orientation="Horizontal"
                Grid.Column="0">
                <Button
                    Command="{Binding EditCommand, Mode=OneWay}"
                    Visibility="{Binding IsEditButtonVisible, Converter={StaticResource BoolToVisConverter}}"
                    Style="{StaticResource DeviceInfoEditButtonStyle}">
                    Edit
                </Button>
            </StackPanel>
            <StackPanel
                Orientation="Horizontal"
                Grid.Column="0"
                Visibility="{Binding IsEditButtonVisible, Converter={StaticResource BoolToVisConverter}, ConverterParameter=Inverse}">
                <Button
                    Command="{Binding OkCommand, Mode=OneWay}"
                    Style="{StaticResource DeviceInfoEditOkButtonStyle}">
                    OK
                </Button>
                <Button
                    Command="{Binding CancelCommand, Mode=OneWay}"
                    Style="{StaticResource DeviceInfoEditCancelButtonStyle}">
                    CANCEL
                </Button>
            </StackPanel>
        </Grid>

    </StackPanel>

</UserControl>

Here is the ViewModel for the User Control:

namespace StagingApp.Presentation.ViewModels.Common;
public partial class DeviceInfoRowViewModel : BaseViewModel
{
    private string? _labelText;

    public string? LabelText
    {
        get => _labelText;
        set 
        { 
            _labelText = value;
            OnPropertyChanged(nameof(LabelText));
        }
    }

    private string? _infoTextBox;

    public string? InfoTextBox
    {
        get => _infoTextBox;
        set 
        { 
            _infoTextBox = value;
            OnPropertyChanged(nameof(InfoTextBox));
        }
    }

    private bool _isEditButtonVisible;

    public bool IsEditButtonVisible
    {
        get => _isEditButtonVisible;
        set 
        {
            _isEditButtonVisible = value;
            OnPropertyChanged(nameof(IsEditButtonVisible));
        }
    }



    [RelayCommand]
    public virtual void Ok()
    {
        IsEditButtonVisible = false;
    }

    [RelayCommand]
    public virtual void Cancel()
    {
        IsEditButtonVisible = true;
    }

    [RelayCommand]
    public virtual void Edit()
    {
        IsEditButtonVisible = true;
    }
}

The BaseViewModel just implements ObservableObject and inherits from INotifyPropertyChanged.

This is what I have so far for the KitchenInfoView, which will actually display my rows:

<UserControl 
    x:Class="StagingApp.Main.Controls.InfoViews.KitchenInfoView"
    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" 
    xmlns:viewmodels="clr-namespace:StagingApp.Presentation.ViewModels.InfoViewModels;assembly=StagingApp.Presentation"
    d:DataContext="{d:DesignInstance Type=viewmodels:KitchenInfoViewModel}"
    xmlns:local="clr-namespace:StagingApp.Main.Controls.Common"
    mc:Ignorable="d" 
    d:DesignHeight="725" 
    d:DesignWidth="780"
    Background="{StaticResource Blue}">
    <Grid Margin="20">

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!-- Title -->
        <Label 
            x:Name="ValidationTitle"
            Grid.Row="0"
            Grid.Column="0"
            Grid.ColumnSpan="4"
            Style="{StaticResource DeviceInfoTitleStyle}">
            DEVICE VALIDATION
        </Label>

        <!-- Directions -->
        <TextBlock
                Grid.Row="1"
                Grid.Column="0"
                Grid.ColumnSpan="4"
                Style="{StaticResource TextDirectionStyle}">
                    Please confirm that the following information is correct. 
                    If any setting is incorrect, change the value in the text box and select "Edit". 
                    The value will then be adjusted. Once all values are correct, press 'OK'.
                    The device will then reboot.
        </TextBlock>

        <!-- Data -->
        <StackPanel>
            <ItemsControl ItemsSource="{Binding Rows}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:DeviceInfoRow />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>

        <!-- Buttons -->
        <StackPanel
            Orientation="Horizontal"
            HorizontalAlignment="Center"
            Margin="0 20 0 0"
            Grid.Row="9"
            Grid.Column="1"
            Grid.ColumnSpan="2">

            <Button
                x:Name="OK"
                IsDefault="True"
                Style="{StaticResource DeviceInfoOkButtonStyle}">
                OK
            </Button>
            <Button
                x:Name="Cancel"
                IsCancel="True"
                Style="{StaticResource DeviceInfoCancelButtonStyle}">
                CANCEL
            </Button>
        </StackPanel>

    </Grid>
</UserControl>

Finally, the KitchenInfoViewModel, looks like this at the moment:

public partial class KitchenInfoViewModel : BaseViewModel
{
    [ObservableProperty]
    [Description("Controller Name")]
    private string? _controllerName;

    [ObservableProperty]
    [Description("Controller Number")]
    private string? _controllerNumber;

    [ObservableProperty]
    [Description("BOH Server Name")]
    private string? _bohServerName;

    [ObservableProperty]
    [Description("TERMSTR")]
    private string? _termStr;

    [ObservableProperty]
    [Description("Key Number")]
    private string? _keyNumber;

    [ObservableProperty]
    [Description("IP Address")]
    private string? _ipAddress;

    [ObservableProperty]
    [Description("BOH IP Address")]
    private string? _bohIpAddress;

    private ObservableCollection<DeviceInfoRowViewModel> _rows;

    public ObservableCollection<DeviceInfoRowViewModel> Rows
    {
        get => _rows;
        set
        {
            _rows = value;
            OnPropertyChanged();
        }
    }


    public KitchenInfoViewModel()
    {
        _rows = new ObservableCollection<DeviceInfoRowViewModel>();
        var properties = typeof(KitchenInfoViewModel)
            .GetProperties();


        foreach (var property in properties)
        {
            var attribute = property.GetCustomAttribute(typeof(DescriptionAttribute));
            var description = (DescriptionAttribute)attribute;
            _rows.Add(new DeviceInfoRowViewModel()
            {
                LabelText = description?.Description.ToString(),
                InfoTextBox = ""
            });
        }
    }

}

My goal is to be able to use the DeviceInfoRow over and over again on various forms, with the content of the label coming from the description of the string properties in the VM. The text box should bind to each property.

Is this possible? Am I asking too much? Am I close? I've been banging my head against a wall all day on this.

Thanks in advance for any help.

CodePudding user response:

with the content of the label coming from the description of the string properties in the VM. The text box should bind to each property

You already have DeviceInfoRowViewModel encapsulating all the initial setup and user updates to the TextBox. I think defining all of these ObservableProperties manually is going againts the automation you want to achieve by using reflection in KitchenInfoViewModel constructor!

Take a look at this

public partial class KitchenInfoViewModel : BaseViewModel
{
    public ObservableCollection<DeviceInfoRowViewModel> Rows { get; set; }

    public KitchenInfoViewModel()
    {
        Rows = new ObservableCollection<DeviceInfoRowViewModel>{
            new DeviceInfoRowViewModel
            {
                LabelText = "Controller Name"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "Controller Number"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "BOH Server Name"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "TERMSTR"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "Key Number"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "IP Address"
            },
            new DeviceInfoRowViewModel
            {
                LabelText = "BOH IP Address"
            }
        };
    }
}

You might say

With properties, I can use _ipAddress directly in code, so how can I refer to it directly now?

You might have these literals in .resx file

new DeviceInfoRowViewModel
{
    LabelText = Resources._ipAddress
}

So you whenever you want ipAddress Data, you can retrieve it from Rows

var ipAddressData = Rows.FirstOrDefault(item => item.LabelText == Resources._ipAddress);

You can define this as get-only property if you want to refer to it repeatedly

private DeviceInfoRowViewModel IpAddressData => 
     Rows.FirstOrDefault(item => item.LabelText == Resources._ipAddress);

My advice is to Keep it simple, you can do the same thing in any other View/ViewModel, your UserControl is well designed and reusable, I can't think of a way simpler and requires less-code than this (compared to the current KitchenInfoViewModel you have).

You might say:

With ObservableProperties, I can just remove the property to remove the row from UI

You can remove its definition from Rows to remove the row from UI as well.


Back to your original question..

Is this possible?

If you want to stick with your approach, you can do it like this

In DeviceInfoRowViewModel

public Action<string> OnInfoChanged { set; get; } // <--------------- 1

private string? _infoTextBox;

public string? InfoTextBox
{
    get => _infoTextBox;
    set 
    { 
        _infoTextBox = value;
        OnPropertyChanged(nameof(InfoTextBox));
        OnInfoChanged?.Invoke(value); // <--------------- 2
    }
}

In KitchenInfoViewModel

_rows.Add(new DeviceInfoRowViewModel()
{
    LabelText = description?.Description.ToString(),
    OnInfoChanged = newUsernput => property.SetValue(this, newUsernput, null); // <--------------- 3
});

So when user updates InfoTextBox, the action will assign the new value to the ObservableProperty.

CodePudding user response:

@Harlan, I want to show you an implementation that you might be able to use for your question.

using System.Windows;

namespace Core2022.SO.Harlan.DescriptionShow
{
    public class DescriptionDto
    {
        public string Description { get; }

        public PropertyPath Path { get; }

        public bool IsReadOnly { get; }
        public object? Source { get; }

        public DescriptionDto(string description, PropertyPath path, bool isReadOnly, object? source)
        {
            Description = description ?? string.Empty;
            Path = path;
            IsReadOnly = isReadOnly;
            Source = source;
        }

        public DescriptionDto SetSource(object? newSource)
            => new DescriptionDto(Description, Path, IsReadOnly, newSource);

        public override string ToString() => Description;
    }
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
using System.Windows;

namespace Core2022.SO.Harlan.DescriptionShow
{
    public class DescriptionPropertyList
    {
        public object Source { get; }

        public ReadOnlyCollection<DescriptionDto> Descriptions { get; }

        public DescriptionPropertyList(object source)
        {
            Source = source ?? throw new ArgumentNullException(nameof(source));

            Type sourceType = source.GetType();
            if (!typeDescriptions.TryGetValue(sourceType, out ReadOnlyCollection<DescriptionDto>? descriptions))
            {
                PropertyInfo[] properties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
                DescriptionDto[] descrType = new DescriptionDto[properties.Length];
                for (int i = 0; i < properties.Length; i  )
                {
                    PropertyInfo property = properties[i];
                    string descr = property.GetCustomAttribute<DescriptionAttribute>()?.Description ??
                                    property.Name;
                    descrType[i] = new DescriptionDto(descr, new PropertyPath(property), !property.CanWrite, Empty);
                }
                descriptions = Array.AsReadOnly(descrType);
                typeDescriptions.Add(sourceType, descriptions);
            }

            DescriptionDto[] descrArr = new DescriptionDto[descriptions.Count];
            for (int i = 0; i < descriptions.Count; i  )
            {
                descrArr[i] = descriptions[i].SetSource(source);
            }
            Descriptions = Array.AsReadOnly(descrArr);
        }

        private static readonly object Empty = new object();
        private static readonly Dictionary<Type, ReadOnlyCollection<DescriptionDto>> typeDescriptions
            = new Dictionary<Type, ReadOnlyCollection<DescriptionDto>>();
    }
}
using System.ComponentModel;

namespace Core2022.SO.Harlan.DescriptionShow
{
    public class ExampleClass
    {
        [Description("Controller Name")]
        public string? ControllerName { get; set; }

        [Description("Controller Number")]
        public string? ControllerNumber { get; set; }

        [Description("BOH Server Name")]
        public string? BohServerName { get; set; }

        [Description("TERMSTR")]
        public string? TermStr { get; set; }

        [Description("Key Number")]
        public string? KeyNumber { get; set; }

        [Description("IP Address")]
        public string? IpAddress { get; set; }

        [Description("BOH IP Address")]
        public string? BohIpAddress { get; set; }
    }
}
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Core2022.SO.Harlan.DescriptionShow
{
    [TemplatePart(Name = TextBoxTemplateName, Type = typeof(TextBox))]
    public class DescriptionControl : Control
    {
        private const string TextBoxTemplateName = "PART_TextBox";
        private TextBox? PartTextBox;
        private Binding? TextBinding;
        public override void OnApplyTemplate()
        {
            PartTextBox = GetTemplateChild(TextBoxTemplateName) as TextBox;
            if (PartTextBox is TextBox tbox)
            {
                if (TextBinding is Binding binding)
                {
                    tbox.SetBinding(TextBox.TextProperty, binding);
                }
                else
                {
                    BindingOperations.ClearBinding(tbox, TextBox.TextProperty);
                }
            }
        }

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


        /// <summary>
        /// Data source, path and description of its property.
        /// </summary>
        public DescriptionDto DescriptionSource
        {
            get => (DescriptionDto)GetValue(DescriptionSourceProperty);
            set => SetValue(DescriptionSourceProperty, value);
        }

        /// <summary><see cref="DependencyProperty"/> для свойства <see cref="DescriptionSource"/>.</summary>
        public static readonly DependencyProperty DescriptionSourceProperty =
            DependencyProperty.Register(
                nameof(DescriptionSource),
                typeof(DescriptionDto),
                typeof(DescriptionControl),
                new PropertyMetadata(null, DescriptionSourceChangedCallback));

        private static void DescriptionSourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DescriptionControl descriptionControl = (DescriptionControl)d;
            Binding? binding = null;
            if (e.NewValue is DescriptionDto description)
            {
                binding = new Binding();
                binding.Path = description.Path;
                binding.Source = description.Source;
                if (description.IsReadOnly)
                {
                    binding.Mode = BindingMode.OneWay;
                }
                else
                {
                    binding.Mode = BindingMode.TwoWay;
                }
            }
            descriptionControl.TextBinding = binding;
            if (descriptionControl.PartTextBox is TextBox tbox)
            {
                if (binding is null)
                {
                    BindingOperations.ClearBinding(tbox, TextBox.TextProperty);
                }
                else
                {
                    tbox.SetBinding(TextBox.TextProperty, binding);
                }
            }
        }
    }
}

Default Template in Themes/Generic.xaml file:

    <Style xmlns:dsc="clr-namespace:Core2022.SO.Harlan.DescriptionShow"
           TargetType="{x:Type dsc:DescriptionControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type dsc:DescriptionControl}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <UniformGrid Rows="1">
                            <TextBlock Text="{Binding DescriptionSource.Description, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type dsc:DescriptionControl}}}"/>
                            <TextBox x:Name="PART_TextBox"/>
                        </UniformGrid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace Core2022.SO.Harlan.DescriptionShow
{
    public class DescriptionsListControl : Control
    {
        static DescriptionsListControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DescriptionsListControl), new FrameworkPropertyMetadata(typeof(DescriptionsListControl)));
        }

        /// <summary>
        /// Descriptions List
        /// </summary>
        public ReadOnlyCollection<DescriptionDto> Descriptions
        {
            get => (ReadOnlyCollection<DescriptionDto>)GetValue(DescriptionsProperty);
            private set => SetValue(DescriptionsPropertyKey, value);
        }

        private static readonly ReadOnlyCollection<DescriptionDto> descriptionsEmpty = Array.AsReadOnly(Array.Empty<DescriptionDto>());

        private static readonly DependencyPropertyKey DescriptionsPropertyKey =
            DependencyProperty.RegisterReadOnly(
                nameof(Descriptions),
                typeof(ReadOnlyCollection<DescriptionDto>),
                typeof(DescriptionsListControl),
                new PropertyMetadata(descriptionsEmpty));
        /// <summary><see cref="DependencyProperty"/> for property <see cref="Descriptions"/>.</summary>
        public static readonly DependencyProperty DescriptionsProperty = DescriptionsPropertyKey.DependencyProperty;

        public DescriptionsListControl()
        {
            DataContextChanged  = OnDataContextChanged;
        }

        private DescriptionPropertyList? descriptionPropertyList;
        private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is null)
            {
                descriptionPropertyList = null;
                Descriptions = descriptionsEmpty;
            }
            else
            {
                descriptionPropertyList = new DescriptionPropertyList(e.NewValue);
            }
            Descriptions = descriptionPropertyList?.Descriptions ?? descriptionsEmpty;
        }
    }
}
<Window x:Class="Core2022.SO.Harlan.DescriptionShow.DescriptionsExampleWindow"
        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:Core2022.SO.Harlan.DescriptionShow"
        mc:Ignorable="d"
        Title="DescriptionsExampleWindow" Height="450" Width="800">
    <Window.Resources>
        <CompositeCollection x:Key="items">
            <local:ExampleClass ControllerName="First"/>
            <local:ExampleClass ControllerName="Second"/>
            <local:ExampleClass ControllerName="Third"/>
        </CompositeCollection>
    </Window.Resources>
    <UniformGrid Columns="2">
        <ListBox x:Name="listBox" ItemsSource="{DynamicResource items}"
                 DisplayMemberPath="ControllerName"
                 SelectedIndex="0"/>
        <ContentControl Content="{Binding SelectedItem, ElementName=listBox}">
            <ContentControl.ContentTemplate>
                <DataTemplate>
                    <local:DescriptionsListControl>
                        <Control.Template>
                            <ControlTemplate TargetType="{x:Type local:DescriptionsListControl}">
                                <ItemsControl ItemsSource="{TemplateBinding Descriptions}">
                                    <ItemsControl.ItemTemplate>
                                        <DataTemplate DataType="{x:Type local:DescriptionDto}">
                                            <local:DescriptionControl DescriptionSource="{Binding}"/>
                                        </DataTemplate>
                                    </ItemsControl.ItemTemplate>
                                </ItemsControl>
                            </ControlTemplate>
                        </Control.Template>
                    </local:DescriptionsListControl>
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>
    </UniformGrid>
</Window>

If you are interested in such an implementation, then ask questions - I will try to answer them.

  • Related