【问题标题】:How to Populate a User Control with a Reusable User Control如何使用可重用的用户控件填充用户控件
【发布时间】:2022-10-05 01:15:16
【问题描述】:

我上周问了这个问题(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 = \"\"
            });
        }
    }

}

我的目标是能够在各种表单上一遍又一遍地使用 DeviceInfoRow,标签的内容来自 VM 中字符串属性的描述。文本框应绑定到每个属性。

这可能吗?我要求太多了吗?我接近了吗?我整天都在用头撞墙。

提前感谢您的帮助。

    标签: c# wpf xaml data-binding windows-community-toolkit


    【解决方案1】:

    标签的内容来自 VM 中字符串属性的描述。文本框应绑定到每个属性

    您已经有DeviceInfoRowViewModel 封装了文本框的所有初始设置和用户更新。我认为定义所有这些 ObservableProperties手动与您想要通过在 Kitchen Info ViewModel 构造函数中使用反射来实现的自动化背道而驰!

    看看这个

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

    你可能会说

    有了属性,我可以直接在代码中使用_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 相比)。

    你可能会说:

    使用 Observable 属性,我可以删除该属性以从 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。

    【讨论】:

      【解决方案2】:

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

      如果您对这样的实现感兴趣,请提出问题 - 我会尽力回答。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-08-27
        • 1970-01-01
        • 2011-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多