如何创建一个与另一个网格重叠的网格并在第二个网格中显示第一个网格的某些控件

How to create a grid with another one overlapping it and show certain controls of the first grid in the second

我正在使用文本框和标签创建多个网格。

有些网格有一些共同的文本框,而另一些文本框对于每个网格都是唯一的。

我正在使用可见性 属性 在需要时显示和折叠每个网格。 问题是,有没有办法在网格重叠时将折叠网格中的文本框显示为不同的可见网格?

是否有更好的控件来执行此操作?

下面是我想做的事情的总结示例:

XAML

<ToolBarTray>
    <ToolBar>
        <Button Name="showgrid1" Click="showgrid1_Click"/>
        <Button Name="showgrid2" Click="showgrid2_Click"/>
    </ToolBar>
</ToolBarTray>
<Grid Name="grid1" Visibility="Collapsed">
    <TextBox Name="Common"/>
    <TextBox Name="UniqueTogrid1"/>
</Grid>
<Grid Name="grid2" Visibility="Collapsed">
    <TextBox Name="UniqueTogrid2"/>
</Grid>

C# 背后的代码:

private void showgrid1_Click(object sender, RoutedEventArgs e)
{
    grid1.Visibility = Visibility.Visible;
    Common.Visibility = Visibility.Visible;
    UniqueTogrid1.Visibility = Visibility.Visible;
    grid2.Visibility = Visibility.Collapsed;
}

private void showgrid2_Click(object sender, RoutedEventArgs e)
{
    grid1.Visibility = Visibility.Collapsed;
    grid2.Visibility = Visibility.Visible;
    UniqueTogrid2.Visibility = Visibility.Visible;
    Common.Visibility = Visibility.Visible; //I want to show this textbox without declaring it in grid2 in XAML, while the grids are overlaping.
}

此代码不会在 grid2 中显示 Common 文本框。

即使进行了编辑,问题也不完全清楚。特别是,您的代码示例没有为正在显示的 UI 元素提供任何实际的 values,从而导致可见性的任何变化都没有意义。如果没有数据,谁在乎什么时候可见?

就是说,从您使用 Click 事件作为响应用户输入的方式来看,我怀疑您在某处同样有一些代码明确设置 Text 属性 用于您正在处理的命名 UI 元素。这导致您希望重用 <TextBox Name="Common"/> 元素,以免重复代码。

如果该推论是正确的,甚至接近正确,那么……动机是光荣的,但由于 WPF 的不当使用,您已将自己逼入绝境 API。具体来说,您应该将 UI 元素视为 throw-away 对象,并将程序中所有有趣的部分保存在称为“视图模型”的 non-UI 对象中。将“MVVM”视为一种编程范式。

所谓“throw-away”,我的意思是这些对象是根据框架的需要创建的,以满足 UI 当前状态的目的。他们不应该扮演比这更重要的角色。当我查看您发布的代码示例时,代码中至少有几个主要警告标志:元素有名称,并且 code-behind 正在操纵它们的视觉状态。

WPF 代码well-written几乎不需要这两个特性。 XAML 经常可以 完全 不仅描述 UI 的外观,还描述它如何根据程序的运行改变视觉状态。

好吧,既然已经解释过了,如何实现您的代码,使其 better-suits WPF API,同时具有最少的重复?

我至少可以立即看到几种方法。一种是基本上保留 XAML 的排列,就像您现在得到的那样,但将重要元素移动到适当的视图模型数据结构中。另一种是使用数据模板和多视图模型数据结构根据当前活动的数据自动更新 UI。我将在此处展示这两种方法。

方法 #1:

第一步是创建视图模型。恕我直言,WPF 程序几乎总是从视图模型开始,因为 XAML(用户界面)的存在是为了服务视图模型(程序数据),而不是相反。理想情况下,视图模型不应 任何 依赖于 UI 框架。它代表独立于框架特定事物的程序状态,尽管它通常仍然具有代表 UI 本身的条件方面的状态。

在某些情况下,您会发现您选择使用视图模型作为偶数 more-rigorous 模型 数据结构之间的适配器;这为程序添加了一个新层,允许模型数据结构完全独立于 UI 关注点。对于这个例子,我没有费心。

class ViewModel1 : NotifyPropertyChangedBase
{
    private string _commonText = "default common text view model 1";
    public string CommonText
    {
        get => _commonText;
        set => _UpdateField(ref _commonText, value);
    }

    private string _uniqueText1 = "default unique text #1";
    public string UniqueText1
    {
        get => _uniqueText1;
        set => _UpdateField(ref _uniqueText1, value);
    }

    private string _uniqueText2 = "default unique text #2";
    public string UniqueText2
    {
        get => _uniqueText2;
        set => _UpdateField(ref _uniqueText2, value);
    }

    private int _gridToShow;
    public int GridToShow
    {
        get => _gridToShow;
        set => _UpdateField(ref _gridToShow, value);
    }

    public ICommand SetGridToShowCommand { get; }

    public ViewModel1()
    {
        SetGridToShowCommand = new SetGridToShow(this);
    }

    private class SetGridToShow : ICommand
    {
        private readonly ViewModel1 _owner;

        public SetGridToShow(ViewModel1 owner)
        {
            _owner = owner;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) => true;

        public void Execute(object parameter)
        {
            if (parameter is int index ||
                (parameter is string text && int.TryParse(text, out index)))
            {
                _owner.GridToShow = index;
            }
        }
    }
}

此 class 具有 WPF 视图模型的一些典型功能:

  1. 它继承了一个基 class 来完成实现 INotifyPropertyChanged 的实际工作。
  2. 它有 public 表示程序当前状态的属性,这些属性将在 XAML 中声明的绑定中使用,以显示特定值或控制某些特定状态UI.
  3. 它有 public 个属性(好吧,在本例中是一个)用于命令对用户输入做出反应。在这个特定的例子中,单个命令的实现是一个独立的嵌套 class,但在 real-world 程序中,这通常也会被概括,使用 helper classes 来做这样的事情处理命令参数的类型转换并接受委托以实际执行命令。

在此示例中,视图模型包含三个 string 属性,一个表示两个 UI 状态之间的共享值,然后是另外两个,每个都是“唯一”每个状态的值,代表当前 UI 状态的 int 属性,以及处理用户输入的 ICommand 属性。

声明了视图模型后,现在我们可以查看 XAML:

<DockPanel>
  <DockPanel.DataContext>
    <l:ViewModel1/>
  </DockPanel.DataContext>
  <ToolBarTray DockPanel.Dock="Top">
    <ToolBar>
      <Button Content="Show Grid 1" Command="{Binding SetGridToShowCommand}" CommandParameter="1"/>
      <Button Content="Show Grid 2" Command="{Binding SetGridToShowCommand}" CommandParameter="2"/>
    </ToolBar>
  </ToolBarTray>
  <Grid>
    <StackPanel>
      <StackPanel.Style>
        <Style TargetType="StackPanel">
          <Setter Property="Visibility" Value="Collapsed"/>
          <Style.Triggers>
            <DataTrigger Binding="{Binding GridToShow}" Value="1">
              <Setter Property="Visibility" Value="Visible"/>
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </StackPanel.Style>
      <TextBox Text="{Binding CommonText}"/>
      <TextBox Text="{Binding UniqueText1}"/>
    </StackPanel>
    <StackPanel>
      <StackPanel.Style>
        <Style TargetType="StackPanel">
          <Setter Property="Visibility" Value="Collapsed"/>
          <Style.Triggers>
            <DataTrigger Binding="{Binding GridToShow}" Value="2">
              <Setter Property="Visibility" Value="Visible"/>
            </DataTrigger>
          </Style.Triggers>
        </Style>
      </StackPanel.Style>
      <TextBox Text="{Binding CommonText}"/>
      <TextBox Text="{Binding UniqueText2}"/>
    </StackPanel>
  </Grid>
</DockPanel>

以上与您的问题相关的重要部分是:

  1. 最重要的是,CommonText 属性 绑定到 两个不同的 TextBox 元素。 IE。 XAML 元素未共享(这本来是对您问题的字面答案),而是共享了基础视图模型 属性。这允许 UI 以适合给定 UI 状态的任何方式与用户交互,而在视图模型中只有一个状态表示用户的输入
  2. 视图模型对象通过 DockPanel.DataContext 元素绑定设置为这部分可视化树的数据上下文。
  3. 用户输入不是通过 Click 事件的处理程序实现的,而是通过根据输入更新视图模型状态的 ICommand 实现的。
  4. UI 状态本身通过为每个“网格”设置的 Style 元素中提供的 DataTrigger 元素响应视图模型的变化(我使用了 StackPanel 而不是本例中的 Grid,因为它更方便,但无论如何都适用相同的一般思想。)

方法 #2:

我认为仅此示例就足以解决您描述的场景。但是,WPF 还可以通过数据模板机制为给定的数据上下文对象显示完全不同的 UI 元素配置。如果我们将这个想法应用到您的问题中,我们可以:

  1. 再建立几个视图模型对象来表示程序中的“唯一”值。
  2. 为每个视图模型对象声明一个模板。
  3. 让 WPF 通过模板自动更新状态,而不是使用 DataTrigger 来更改 UI 的视觉状态,只需更新当前显示的视图模型即可。

在这个方案中,这是我想出的视图模型对象……

主要的:

class ViewModel2 : NotifyPropertyChangedBase
{
    private readonly ViewModel2A _viewModel2A = new ViewModel2A();
    private readonly ViewModel2B _viewModel2B = new ViewModel2B();

    public string CommonText => "common text view model 2";

    private object _gridViewModel;
    public object GridViewModel
    {
        get => _gridViewModel;
        set => _UpdateField(ref _gridViewModel, value);
    }

    public ICommand SetGridToShowCommand { get; }

    public ViewModel2()
    {
        SetGridToShowCommand = new SetGridToShow(this);
    }

    private class SetGridToShow : ICommand
    {
        private readonly ViewModel2 _owner;

        public SetGridToShow(ViewModel2 owner)
        {
            _owner = owner;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) => true;

        public void Execute(object parameter)
        {
            if (parameter is int index ||
                (parameter is string text && int.TryParse(text, out index)))
            {
                _owner.SetGridToShowIndex(index);
            }
        }
    }

    private void SetGridToShowIndex(int index)
    {
        GridViewModel = index == 1 ? (object)_viewModel2A : _viewModel2B;
    }
}

还有两个“独特”的:

class ViewModel2A
{
    public string UniqueText1 => "unique text grid #1";
}

class ViewModel2B
{
    public string UniqueText2 => "unique text grid #2";
}

出于本示例的目的,我跳过了 INotifyPropertyChanged,只是制作了具有 read-only/display-only 属性的视图模型。

请注意,在主视图模型中,当用户输入发生时,它所做的只是将当前“网格”视图模型设置为适当的“唯一”视图模型对象。

有了这个,我们可以写 XAML:

<DockPanel Grid.Column="1">
  <DockPanel.DataContext>
    <l:ViewModel2/>
  </DockPanel.DataContext>
  <DockPanel.Resources>
    <DataTemplate DataType="{x:Type l:ViewModel2A}">
      <StackPanel>
        <!-- OneWay binding for illustration purposes (view model property is read-only) -->
        <!-- RelativeSource allows for referencing properties from other than the current data context, such as the common text property -->
        <TextBox Text="{Binding DataContext.CommonText, Mode=OneWay, RelativeSource={RelativeSource AncestorType=DockPanel}}"/>
        <TextBox Text="{Binding UniqueText1, Mode=OneWay}"/>
      </StackPanel>
    </DataTemplate>
    <DataTemplate DataType="{x:Type l:ViewModel2B}">
      <StackPanel>
        <TextBox Text="{Binding DataContext.CommonText, Mode=OneWay, RelativeSource={RelativeSource AncestorType=DockPanel}}"/>
        <TextBox Text="{Binding UniqueText2, Mode=OneWay}"/>
      </StackPanel>
    </DataTemplate>
  </DockPanel.Resources>
  <ToolBarTray DockPanel.Dock="Top">
    <ToolBar>
      <Button Content="Show Grid 1" Command="{Binding SetGridToShowCommand}" CommandParameter="1"/>
      <Button Content="Show Grid 2" Command="{Binding SetGridToShowCommand}" CommandParameter="2"/>
    </ToolBar>
  </ToolBarTray>
  <Grid>
    <ContentControl Content="{Binding GridViewModel}"/>
  </Grid>
</DockPanel>

在这里,父 DockPanel 元素的资源字典中声明了两个不同的模板,而不是使用触发器设置样式,每个模板对应一个“唯一”视图模型类型。然后在 Grid 控件中,内容简单地绑定到当前“唯一”视图模型对象。 WPF 将 select 根据当前“唯一”视图模型对象的类型生成正确的模板。

我在上面的 XAML 中做的一件稍微复杂的事情是将 CommonText 属性 放在主视图模型中,使其实际上对两种视图状态都是通用的。然后模板都通过使用 RelativeSource 模式进行绑定来引用它。也可以改为让数据模板 为“唯一”属性提供 UI 元素,并让父 [​​=153=] 元素处理显示CommonText 属性。这可以说会更简洁、重复性更少,但它也与您最初发布的代码有足够大的差异,因此我决定不跨过那座桥。 :)


最后,以上所有内容都依赖于我之前提到的基础 class 来实现 INotifyPropertyChanged。有多种实现方法,但为了完成上面的示例,这里是我用于上面代码的实现:

class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}