简体   繁体   中英

How to Populate a User Control with a Reusable User Control

I asked this question ( How to Toggle Visibility Between a Button and a Stack Panel Containing Two Buttons ) last week, and the answer was perfect and exactly what I was looking for. I realized though that I'm going to have 3 User Controls all with the very similar elements on them, so it would be better to split the row out to be re-usable. But, I am having a really hard time getting the controls to display on the form.

Here is the end result that I am looking for: 在此处输入图像描述

I have created this User Control, DeviceInfoRow.xaml: 在此处输入图像描述

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.

EDIT:

To explain what I'm trying to do a little bit better:

This is a single window application. Depending on the type of device that this application will be deployed on will determine which initial view the ShellView will load. There are three different configurations: Server, Kitchen, Terminal. But each configuration is very similar:

Kitchen Configure View厨房配置视图

Server Configure View服务器配置视图

Terminal Configure View终端配置视图

As you can see, each of the three views are very similar, even capturing the similar information. What is meant to happen is that after the user inputs the information into the form, they will select "Configure" and the device will automatically be configured, using the information provided. That information will be stored in a separate model to be used by the information.

Once the device is finished being configured, the Device Validation form (a modal form) will pop up, alerting the user that the configuration is finished and to double check that the device was configured correctly (because these are Windows devices and sometimes things don't work the way they should). Initially all of the information on the Device Validation form was static, but I had the idea to populate a text box with the device information (coming from the device). This way, if something is incorrect, the user can press "Edit", enter the correct information in the text box and then press "OK". The application at that point will make the necessary changes to the device, and then re-display the form.

Again each form has a very similar look to it, with a label (or text block), text box, and a couple of buttons for each row. The Configure forms also are very similar, with a label and text box. What I'm looking for is instead of copying and pasting each row in the XAML file, having a template row (one for Validation, one for Configure) that can then be populated with the Description attribute of the properties (for the Label portion) and bound to the individual properties (for the TextBox portion).

I really hope this clears things up and makes sense.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM