WPF一个好的制作视图/编辑控件的方法是什么?

7

这只是一个讨论问题 - 在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
这只是一个讨论问题 - 目前还没有太多麻烦,但我想知道是否有其他可能性和优雅的解决方案。
4个回答

4

自动锁定类

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

将“DoLock”属性设置为true,将重新设计所有TextBoxes ComboBoxes、CheckBoxes等,使它们成为TextBlocks、不可编辑的CheckBoxes等。我的代码设置了其他附加属性,可以指定在锁定(“查看”)模式下使用任意模板的控件,永远不应自动锁定的控件等。

因此,相同的视图可以很容易地用于编辑和查看。设置单个属性即可来回切换,并且它是完全可自定义的,因为视图中的任何控件都可以针对“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标准控件的默认模板在ResourceDictionary中定义,该ResourceDictionary合并到Themes/Generic.xaml中,位于包含AutomaticLock类的程序集中。例如,此模板会使所有TextBox在锁定时变成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. 当您想要锁定时,在顶级控件(窗口、视图、UserControl等)上设置AutomaticLock.DoLock="True",以便在您需要自动锁定实际发生时进行操作。您可以将AutomaticLock.DoLock绑定到复选框或菜单项,或者在代码中进行控制。
一些有效切换视图和编辑模式的技巧:
即使视图和编辑模式有很大的区别,使用此AutomaticLock类也非常适合切换两者。我有几种不同的构建视图的技术,以适应编辑时的布局差异。其中一些是:
1. 通过将它们的模板或自动锁定模板设置为空模板,使控件在编辑或查看模式下不可见。例如,假设“年龄”在视图模式下位于布局顶部,在编辑模式下位于底部。在两个位置都添加一个“年龄”文本框。在顶部的文本框中将模板设置为空模板,以便在编辑模式下不显示它。在底部的文本框中将AutomaticLockTemplate设置为空模板。现在只有一个会显示出来。
2. 使用ContentControl替换包围内容的边框、布局面板、按钮等,而不影响内容。ContentControl的模板具有编辑模式下的周围边框、面板、按钮等。它还具有一个自动锁定模板,其中包含视图模式版本。
3. 使用Control替换视图中的矩形部分(我实际上是指“Control”类的对象,而不是子类)。同样,您可以将编辑模式版本放入模板中,并将视图模式版本放入AutomaticLockTemplate中。
4. 使用带有额外自动大小行和列的Grid。使用AutomaticLock.DoLock属性上的触发器来更新Grid内项目的Row、Column、RowSpan和ColumnSpan属性。例如,您可以通过将其Grid.Row从6更改为0来将包含“年龄”控件的面板移动到顶部。
5. 触发DoLock以对您的项目应用LayoutTranform或RenderTransform,或设置其他属性,如宽度和高度。如果您想要在编辑模式下使事物变大,或者如果您想要使TextBox更宽并将按钮移动到其边缘旁边,则这非常有用。
请注意,您可以在整个视图中使用选项#3(带有编辑和查看模式的控制对象)。如果编辑和查看模式完全不同,则可以这样做。在这种情况下,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也具备这种功能。


1
你能否分享一下你的AutomaticLock类的示例? - Metro Smurf
这段代码看起来很棒 :) 唯一的缺点可能是无法重新构建视图/编辑(例如,如果我们需要“查看”模式与“编辑”模式不同)。但这不是解决方案的问题,因为它不是为此目的而制作的。我一定会在我的项目中尝试这个! - Jefim
确实,当您的视图和编辑模式布局相似时,AutomaticLock提供了最大的价值,但是使用AutomaticLock类在切换视图和编辑模式时重新构建UI并不是不可能的。事实上,这非常容易。我扩展了我的答案,以解释我用于处理模式之间布局更改的一些技术以及它们如何利用AutomaticLock。 - Ray Burns
我相信贴出来的代码需要进行一些小改动。在DoLockProperty定义中,typeof(ControlTemplate) 应该被更改为 typeof(AutomaticLock): public static readonly DependencyProperty DoLockProperty = DependencyProperty.RegisterAttached("DoLock", typeof(bool), typeof(AutomaticLock)... - Scott O.
感谢这个优秀的课程,我们正在使用它,现在我正在尝试自定义它,但是我无法理解OnLockingStateChanged中的这行代码 new AutomaticLock((Control)obj).Attach();,其中分配了新的引用。 - AbderrahimDz24

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代码(例如,所有控件的可见性都必须绑定;在我的情况下,只有两个选项卡的两个绑定,但这意味着我必须复制第二个选项卡中的所有控件)。 这使代码变得有点难以阅读...再次强调,这是一种做事方式的问题。感谢回答! - Jefim

0
我会创建一个单一的视图,其中包含2个不同的配置选项,例如2个不同的构造函数,以使相关字段可编辑/只读或可见/隐藏。
这样做的好处是您不需要编写冗余的XAML代码,并且在使用MVVM时可以通过代码后台或ViewModel配置所有字段。

0

听起来对我来说是一个DataTemplateSelector的工作。如果你宁愿在原地切换单个控件,我会做类似于Veer建议的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接