簡體   English   中英

如何使用可重用的用戶控件填充用戶控件

[英]How to Populate a User Control with a Reusable User Control

上周我問了這個問題( How to Toggle Visibility Between a Button and a Stack Panel Containing Two Buttons ),答案很完美,正是我想要的。 我意識到我將擁有 3 個用戶控件,它們上面都有非常相似的元素,所以最好將行拆分出來以便重復使用。 但是,我真的很難讓控件顯示在表單上。

這是我正在尋找的最終結果: 在此處輸入圖像描述

我創建了這個用戶控件 DeviceInfoRow.xaml: 在此處輸入圖像描述

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

這是用戶控件的 ViewModel:

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

BaseViewModel只是實現ObservableObject並繼承自INotifyPropertyChanged

到目前為止,這是我對 KitchenInfoView 的了解,它實際上會顯示我的行:

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

最后,KitchenInfoViewModel,目前看起來像這樣:

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

}

我的目標是能夠在各種 forms 上反復使用 DeviceInfoRow,其中 label 的內容來自 VM 中字符串屬性的描述。 文本框應綁定到每個屬性。

這可能嗎? 我要求太多了嗎? 我很接近嗎? 為此,我整天都在用頭撞牆。

在此先感謝您的幫助。

編輯:

為了解釋我想做的更好一點:

這是一個單獨的 window 應用程序。 根據將部署此應用程序的設備類型,將確定 ShellView 將加載哪個初始視圖。 共有三種不同的配置:服務器、廚房、終端。 但是每個配置都非常相似:

廚房配置視圖廚房配置視圖

服務器配置視圖服務器配置視圖

終端配置視圖終端配置視圖

如您所見,三個視圖中的每一個都非常相似,甚至捕獲了相似的信息。 意味着發生的是,在用戶將信息輸入表單后,他們將 select “配置”,設備將使用提供的信息自動配置。 該信息將存儲在單獨的 model 中以供信息使用。

設備配置完成后,設備驗證表單(模態表單)將彈出,提醒用戶配置已完成並仔細檢查設備配置是否正確(因為這些是 Windows 設備,有時事情不t 以他們應該的方式工作)。 最初設備驗證表單上的所有信息都是 static,但我想用設備信息(來自設備)填充一個文本框。 這樣,如果出現錯誤,用戶可以按“編輯”,在文本框中輸入正確的信息,然后按“確定”。 此時應用程序將對設備進行必要的更改,然后重新顯示表單。

同樣,每個表單都具有非常相似的外觀,每行都有一個 label(或文本塊)、文本框和幾個按鈕。 Configure forms 也非常相似,有一個 label 和文本框。 我正在尋找的不是復制和粘貼 XAML 文件中的每一行,而是有一個模板行(一個用於驗證,一個用於配置)然后可以用屬性的描述屬性填充(對於 Label 部分)並綁定到各個屬性(對於 TextBox 部分)。

我真的希望這可以解決問題並且有意義。

label 的內容來自 VM 中字符串屬性的描述。 文本框應綁定到每個屬性

您已經擁有將所有初始設置和用戶更新封裝到 TextBox 的DeviceInfoRowViewModel 我認為手動定義所有這些 ObservableProperties 與您希望通過在 KitchenInfoViewModel 構造函數中使用反射來實現的自動化背道而馳!

看看這個

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

你可能會說

有了properties,我就可以直接在代碼中使用_ipAddress了,那么現在怎么直接引用呢?

您可能在 .resx 文件中有這些文字

new DeviceInfoRowViewModel
{
    LabelText = Resources._ipAddress
}

因此,無論何時您需要 ipAddress 數據,都可以從Rows中檢索它

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

如果你想重復引用它,你可以將它定義為 get-only 屬性

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

我的建議是保持簡單,您可以在任何其他 View/ViewModel 中做同樣的事情,您的 UserControl 設計精良且可重用,我想不出比這更簡單且需要更少代碼的方法(與當前的相比KitchenInfoViewModel 你有)。

你可能會說:

使用 ObservableProperties,我只需刪除屬性即可從 UI 中刪除行

您可以從Rows中刪除它的定義,從而也從 UI 中刪除該行。


回到你原來的問題..

這可能嗎?

如果你想堅持你的方法,你可以這樣做

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

KitchenInfoViewModel

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

因此,當用戶更新InfoTextBox時,該操作會將新值分配給 ObservableProperty。

@Harlan,我想向您展示一個可以用於您的問題的實現。

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

Themes/Generic.xaml 文件中的默認模板:

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

如果您對這樣的實現感興趣,請提出問題 - 我會盡力回答。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM