【问题标题】:WPF A good way to make a view/edit control?WPF 制作视图/编辑控件的好方法?
【发布时间】:2023-03-15 12:01:01
【问题描述】:

这只是一个需要讨论的问题 - 在 WPF 中制作视图/编辑控件的最佳方法是什么?例如。我们有一个实体对象 Person,它有一些属性(姓名、姓氏、地址、电话等)。控件的一种表示形式是只读视图。另一个将拥有同一个人的编辑视图。示例:

<UserControl x:Name="MyPersonEditor">
    <Grid>
        <Grid x:Name="ViewGrid" Visibility="Visible">
            <TextBlock Text="Name:"/>
            <TextBlock Text="{Binding Person.Name}"/>
            <Button Content="Edit" Click="ButtonEditStart_Click"/>
        </Grid>

        <Grid x:Name="EditGrid" Visibility="Collapsed">
            <TextBlock Text="Name:"/>
            <TextBox Text="{Binding Person.Name}"/>
            <Button Content="Save" Click="ButtonEditEnd_Click"/>
        </Grid>
    </Grid>
</UserControl>

我希望这个想法很清楚。我现在看到的两个选项

  1. 两个带有可见性切换的网格和
  2. 没有标题面板的 TabControl

这只是一个讨论问题 - 没什么麻烦,但我只是想知道是否有任何其他可能性和优雅的解决方案。

【问题讨论】:

    标签: wpf controls crud


    【解决方案1】:

    自动锁定类

    我编写了一个“AutomaticLock”类,它具有继承的附加“DoLock”属性。

    将“DoLock”属性设置为 true 会将所有文本框组合框、复选框等重新模板化为文本块、不可编辑的复选框等。我的代码设置为使其他附加属性可以指定要在锁定(“视图”)模式下使用的任意模板、不应该自动锁定的控件等。

    因此,可以轻松地使用同一个视图进行编辑和查看。设置单个属性来回更改它,并且它是完全可自定义的,因为视图中的任何控件都可以触发“DoLock”属性以任意方式更改其外观或行为。

    实现代码

    代码如下:

    public class AutomaticLock : DependencyObject
    {
      Control _target;
      ControlTemplate _originalTemplate;
    
      // AutomaticLock.Enabled:  Set true on individual controls to enable locking functionality on that control
      public static bool GetEnabled(DependencyObject obj) { return (bool)obj.GetValue(EnabledProperty); }
      public static void SetEnabled(DependencyObject obj, bool value) { obj.SetValue(EnabledProperty, value); }
      public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(AutomaticLock), new FrameworkPropertyMetadata
      {
        PropertyChangedCallback = OnLockingStateChanged,
      });
    
      // AutomaticLock.LockTemplate:  Set to a custom ControlTemplate to be used when control is locked
      public static ControlTemplate GetLockTemplate(DependencyObject obj) { return (ControlTemplate)obj.GetValue(LockTemplateProperty); }
      public static void SetLockTemplate(DependencyObject obj, ControlTemplate value) { obj.SetValue(LockTemplateProperty, value); }
      public static readonly DependencyProperty LockTemplateProperty = DependencyProperty.RegisterAttached("LockTemplate", typeof(ControlTemplate), typeof(AutomaticLock), new FrameworkPropertyMetadata
      {
        PropertyChangedCallback = OnLockingStateChanged,
      });
    
      // AutomaticLock.DoLock:  Set on container to cause all children with AutomaticLock.Enabled to lock
      public static bool GetDoLock(DependencyObject obj) { return (bool)obj.GetValue(DoLockProperty); }
      public static void SetDoLock(DependencyObject obj, bool value) { obj.SetValue(DoLockProperty, value); }
      public static readonly DependencyProperty DoLockProperty = DependencyProperty.RegisterAttached("DoLock", typeof(bool), typeof(ControlTemplate), new FrameworkPropertyMetadata
      {
        Inherits = true,
        PropertyChangedCallback = OnLockingStateChanged,
      });
    
      // CurrentLock:  Used internally to maintain lock state
      [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
      public static AutomaticLock GetCurrentLock(DependencyObject obj) { return (AutomaticLock)obj.GetValue(CurrentLockProperty); }
      public static void SetCurrentLock(DependencyObject obj, AutomaticLock value) { obj.SetValue(CurrentLockProperty, value); }
      public static readonly DependencyProperty CurrentLockProperty = DependencyProperty.RegisterAttached("CurrentLock", typeof(AutomaticLock), typeof(AutomaticLock));
    
    
      static void OnLockingStateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
      {
        AutomaticLock current = GetCurrentLock(obj);
        bool shouldLock = GetDoLock(obj) && (GetEnabled(obj) || GetLockTemplate(obj)!=null);
        if(shouldLock && current==null)
        {
          if(!(obj is Control)) throw new InvalidOperationException("AutomaticLock can only be used on objects derived from Control");
          new AutomaticLock((Control)obj).Attach();
        }
        else if(!shouldLock && current!=null)
          current.Detach();
      }
    
      AutomaticLock(Control target)
      {
        _target = target;
      }
    
      void Attach()
      {
        _originalTemplate = _target.Template;
        _target.Template = GetLockTemplate(_target) ?? SelectDefaultLockTemplate();
        SetCurrentLock(_target, this);
      }
    
      void Detach()
      {
        _target.Template = _originalTemplate;
        _originalTemplate = null;
        SetCurrentLock(_target, null);
      }
    
      ControlTemplate SelectDefaultLockTemplate()
      {
        for(Type type = _target.GetType(); type!=typeof(object); type = type.BaseType)
        {
          ControlTemplate result =
            _target.TryFindResource(new ComponentResourceKey(type, "AutomaticLockTemplate")) as ControlTemplate ??
            _target.TryFindResource(new ComponentResourceKey(typeof(AutomaticLock), type.Name)) as ControlTemplate;
          if(result!=null) return result;
        }
        return null;
      }
    }
    

    此代码将允许您在逐个控件的基础上指定自动锁定模板,或者它将允许您使用在包含 AutomaticLock 类的程序集中定义的默认模板,在包含您的自定义控件的程序集中定义锁定模板适用于,在您的可视化树中的本地资源(包括您的应用程序资源)

    如何定义 AutomaticLock 模板

    WPF 标准控件的默认模板在合并到 Themes/Generic.xaml 的 ResourceDictionary 中定义在包含 AutomaticLock 类的程序集中。例如,此模板在锁定时会导致所有 TextBoxes 变成 TextBlocks:

    <ControlTemplate TargetType="{x:Type TextBox}"
      x:Key="{ComponentResourceKey ResourceId=TextBox, TypeInTargetAssembly={x:Type lc:AutomaticLock}}">
      <TextBlock Text="{TemplateBinding Text}" />
    </ControlTemplate>
    

    自定义控件的默认模板可以在包含自定义控件的程序集中在合并到其 Themes/Generic.xaml 中的 ResourceDictionary 中定义。在这种情况下 ComponentResourceKey 是不同的,例如:

    <ControlTemplate TargetType="{x:Type prefix:MyType}"
      x:Key="{ComponentResourceKey ResourceId=AutomaticLockTemplate, TypeInTargetAssembly={x:Type prefix:MyType}}">
        ...
    

    如果应用程序想要覆盖特定类型的标准 AutomaticLock 模板,它可以将自动锁定模板放置在其 App.xaml、Window XAML、UserControl XAML 或单个控件的 ResourceDictionary 中。在每种情况下,ComponentResourceKey 的指定方式都应与自定义控件相同:

    x:Key="{ComponentResourceKey ResourceId=AutomaticLockTemplate, TypeInTargetAssembly={x:Type prefix:MyType}}"
    

    最后,可以通过设置AutomaticLock.LockTemplate 属性将自动锁定模板应用于单个控件。

    如何在 UI 中使用 AutomaticLock

    使用自动锁定:

    1. 在任何应自动锁定的控件上设置 AutomaticLock.Enabled="True"。这可以以样式或直接在单个控件上完成。它启用对控件的锁定,但不会导致控件实际锁定。
    2. 当您想要锁定时,只要您希望自动锁定实际发生,就在您的顶级控件(窗口、视图、用户控件等)上设置 AutomaticLock.DoLock="True"。您可以将 AutomaticLock.DoLock 绑定到复选框或菜单项,也可以在代码中对其进行控制。

    在查看和编辑模式之间有效切换的一些技巧

    这个 AutomaticLock 类非常适合在视图和编辑模式之间切换,即使它们有很大的不同。我有几种不同的技术来构建我的视图以适应编辑时的布局差异。其中一些是:

    1. 根据具体情况,通过将其 Template 或 AutomaticLockTemplate 设置为空模板,使控件在编辑或查看模式下不可见。例如,假设“年龄”在视图模式下位于布局的顶部,而在编辑模式下位于底部。在两个地方为“年龄”添加一个文本框。在顶部一个设置模板为空模板,因此它不会在编辑模式下显示。在底部将 AutomaticLockTemplate 设置为空模板。现在一次只能看到一个。

    2. 使用 ContentControl 替换内容周围的边框、布局面板、按钮等,而不影响内容。 ContentControl 的模板具有用于编辑模式的周围边框、面板、按钮等。它还有一个具有查看模式版本的 AutomaticLockTemplate。

    3. 使用控件替换视图的矩形部分。 (我实际上是指“Control”类的对象,而不是其子类。)同样,您将编辑模式版本放在模板中,将查看模式版本放在 AutomaticLockTemplate 中。

    4. 使用具有额外自动调整大小的行和列的网格。使用 AutomaticLock.DoLock 属性上的触发器来更新 Grid 中项目的 Row、Column、RowSpan 和 ColumnSpan 属性。例如,您可以通过将 Grid.Row 从 6 更改为 0 来将包含“年龄”控件的面板移动到顶部。

    5. 触发 DoLock 以将 LayoutTranform 或 RenderTransform 应用于您的项目,或设置其他属性,如宽度和高度。如果您希望在编辑模式下使内容更大,或者如果您想让 TextBox 更宽并将其旁边的按钮移到边缘,这将非常有用。

    请注意,您可以对整个视图使用选项 #3(具有单独的编辑和查看模式模板的 Control 对象)。如果编辑和查看模式完全不同,就会这样做。在这种情况下,AutomaticLock 仍然为您提供了手动设置两个模板的便利。它看起来像这样:

    <Control>
      <Control.Template>
        <ControlTemplate>
          <!-- Edit mode view here -->
        </ControlTemplate>
      </Control.Template>
      <lib:AutomaticLock.LockTemplate>
        <ControlTemplate>
          <!-- View mode view here -->
        </ControlTemplate>
      </lib:AutomaticLock.LockTemplate>
    </Control>
    

    一般来说,在编辑和查看模式之间调整一些小位置和东西会更容易,并且更好地为您的用户体验,因为用户将拥有一致的布局,但如果您确实需要一个完整的替换 AutomaticLock 为您提供这样的功能嗯。

    【讨论】:

    • 能否分享您的 AutomaticLock 类示例?
    • 这段代码看起来很棒 :) 一个缺点可能是无法重构视图/编辑(例如,如果我们需要“视图”模式与“编辑”模式不同)。但这不是解决方案的问题,因为它不是为此目的而设计的。我一定会在我的项目中尝试一下!
    • 当您的视图和编辑模式布局彼此相似时,AutomaticLock 确实提供了最大值,但是当使用 AutomaticLock 类时,在切换时重构 UI 并非不可能查看和编辑模式。事实上,这非常容易。我已经扩展了我的答案,以解释我用于处理模式之间布局更改的一些技术以及它们如何利用 AutomaticLock。
    • 我相信上面发布的代码需要做一些小改动。在 DoLockProperty 定义中,typeof(ControlTemplate) 应该是 typeof(AutomaticLock):public static readonly DependencyProperty DoLockProperty = DependencyProperty.RegisterAttached("DoLock", typeof(bool), typeof(AutomaticLock)...
    • 感谢这个伟大的课程,我们正在使用它,现在我正在尝试自定义它,但我无法理解 OnLockingStateChanged new AutomaticLock((Control)obj).Attach(); 中的这行代码。分配新引用的位置。
    【解决方案2】:
    <Grid>
        <TextBlock Text="Name:"/> 
        <LabelText="{Binding Person.Name}" Cursor="IBeam" MouseDoubleClick="lblName_dblClick"/>  <!-- set the IsEditMode to true inside this event -->
        <TextBox Text="{Binding Person.Name}" Visibility="{Binding IsEditMode, Converter={StaticResource BoolToVisConverter}}"/>
        <Button Content="OK" Click="btnSave_Click" Visibility="{Binding IsEditMode, Converter={StaticResource BoolToVisConverter}}"/> <!-- set the IsEditMode to false inside this event -->
    </Grid>
    

    如果您熟悉,请使用命令。

    【讨论】:

    • 我目前有一个非常相似的解决方案,但更改了依赖于 IsEditMode 属性的面板。非常相似的工作机制(只是在不同级别的元素上)。无论如何,我在这里提出问题的原因是这种方法带来了大量的 XAML(例如,必须绑定所有控件的可见性;在我的情况下,两个选项卡只有两个绑定,但这意味着我必须复制第二个选项卡中的所有控件)。这使代码的可读性降低了……再一次,如何去做是一个品味问题。感谢您的回复!
    【解决方案3】:

    我会创建一个具有 2 个不同配置选项的视图,例如2 个不同的构造函数,使相关字段可编辑/只读或可见/隐藏

    这样您就不会编写多余的 XAML,并且可以在使用 MVVM 时通过代码或 ViewModel 配置所有字段

    【讨论】:

      【解决方案4】:

      对我来说,这听起来像是 DataTemplateSelector 的工作。如果您更愿意将各个控件切换到位,我会做类似于 Veer 建议的操作。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-06
        • 2017-08-28
        • 2011-07-09
        • 2020-09-21
        • 1970-01-01
        • 2014-08-17
        • 2011-11-03
        • 2011-08-04
        相关资源
        最近更新 更多