如何使内容动态呈现而不更改数据上下文?

3
我已经设置了一个MVVM框架来在视图之间切换。为适应设计,主窗口包含一个选项卡控制器,根据需要显示相应的页面。当用户按下按钮时,其中一个内部页面将发生更改。以下是该设置的可视化表示: 该设置的可视化表示 我将一个名为Presenter的视图模型设置为StudentView的数据上下文,以处理在StudentOverview中引发的按钮事件。那样可以工作,但当我想切换视图时,我必须设置一个特定类型的新数据上下文。但由于Presenter是我的数据上下文,所以切换它会删除按钮的功能。
我想要的是在不依赖数据上下文的情况下更改数据模板。

StudentView.xaml

<Page {...}>
    
    <Page.DataContext>
        <viewModels:Presenter/>
    </Page.DataContext>
    <Page.Resources>
        <DataTemplate x:Key="Overview" DataType="{x:Type models:StudentOverviewModel}">
            <local:StudentOverview/>
        </DataTemplate>
        <DataTemplate x:Key="Add" DataType="{x:Type models:StudentAddModel}">
            <local:AddStudentControl/>
        </DataTemplate>
    </Page.Resources>
    <ContentPresenter Content="{Binding}"/>
</Page>

StudentView.xaml.cs

public partial class StudentView : Page
    {
        public StudentView()
        {
            InitializeComponent();

            // This switches the view but disables the button
            this.DataContext = new StudentOverviewModel();

            if (this.DataContext is Presenter presenter)
            {
                presenter.PropertyChanged += (object o, PropertyChangedEventArgs e) =>
                {
                    // This switches the view but disables the button
                    this.DataContext = new StudentAddModel();
                };
            }
        }
    }

只需创建两个不同版本的StudentOverviewModel,它们都继承自相同的基类。使用基类作为变量名,并公开其中一个实例或另一个实例。视图可以决定是否要有单独的模板,但您可能希望为每种类型制作一个模板。| 当然,这可能是那些罕见情况之一,其中按钮实际上是一个仅限于查看的东西,而不是ViewModel应该关心的东西。 - Christopher
我重新阅读了一下,你确切地不想使用子类化。好吧,至少有三种方法可以解决这个问题:https://www.jerriepelser.com/blog/3-ways-dynamic-data-templates/ 其中只有一个是模板选择器。| 但是我认为按钮应该是学生视图的一部分,因为它替换了其中显示的实例。 - Christopher
1个回答

2
我可以提供两个解决方案:
第一个解决方案(推荐)是向 Presenter 视图模型添加一个类型为 object(或所有视图模型的通用基础类型,例如 IContentModel)的 SelectedContent 属性。然后将 SelectedContent 绑定到 ContentPresenter.Content 属性上:
Presenter.cs
public partial class Presenter : INotifyPropertyChanged
{
  public Presenter()
  {
    // Set default content
    this.SelectedContent = new StudentOverviewModel();
  }

  private object selectedContent;
  public object SelectedContent
  {
    get => this.selectedContent; 
    set
    { 
      this.selectedContent = value; 
      OnPropertyChanged();
    }
  }

  // Use ICommand implementation like DelegateCommand
  public ICommand LoadContentCommand => new LoadContentCommand(ExecuteLoadContent, CanExecuteLoadContent);

  private void ExecuteLoadContent(object param)
  {
    // Do something ...

    // Load the new content on Button clicked
    this.SelectedContent = new StudentAddModel();
  }

  private bool CanExecuteLoadContent => true;
}

StudentView.xaml.cs

public partial class StudentView : Page
{
  public StudentView()
  {
    InitializeComponent();
  }
}

StudentView.xaml

<Page {...}>
  <Page.DataContext>
    <viewModels:Presenter/>
  </Page.DataContext>
  <Page.Resources>
    <DataTemplate DataType="{x:Type models:StudentOverviewModel}">
      <local:StudentOverview/>
    </DataTemplate>
    <DataTemplate ="{x:Type models:StudentAddModel}">
      <local:AddStudentControl/>
    </DataTemplate>
  </Page.Resources>

  <ContentPresenter Content="{Binding SelectedContent}"/>
</Page>

StudentOverview.xaml

<UserControl{...}>

  <!--- content -->

  <Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type StudentView}}, Path=DataContext.LoadContentCommand}"/>
</UserControl>

您可以安全地删除DataTemplateKey属性(如果DataType不同),这样它们将自动应用于任何匹配的数据类型(隐式DataTemplate)。


另一种解决方案是将SelectedContent移动到StudentView并将其转换为DependencyProperty

StudentView.xaml.cs

public partial class StudentView : Page
{
  public static readonly DependencyProperty SelectedContentProperty = DependencyProperty.Register(
    "SelectedContent", 
    typeof(object), 
    typeof(StudentView));

  public object SelectedContent
  {
    get => GetValue(SelectedContentProperty); 
    set => SetValue(SelectedContentProperty, value); 
  }

  public StudentView()
  {
    InitializeComponent();

    // This switches the view without disabling the button
    this.SelectedContent = new StudentOverviewModel();

    if (this.DataContext is Presenter presenter)
    {
      presenter.PropertyChanged += (object o, PropertyChangedEventArgs e) =>
      {
        // This switches the view without disabling the button
        this.SelectedContent = new StudentAddModel();               
      };
    }
  }
}

StudentView.xaml

<Page {...}>
  <Page.DataContext>
    <viewModels:Presenter/>
  </Page.DataContext>
  <Page.Resources>
    <DataTemplate DataType="{x:Type models:StudentOverviewModel}">
      <local:StudentOverview/>
    </DataTemplate>
    <DataTemplate ="{x:Type models:StudentAddModel}">
      <local:AddStudentControl/>
    </DataTemplate>
  </Page.Resources>

  <ContentPresenter Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type StudentView}}, Path=SelectedContent}"/>
</Page>

我尝试实现了你的第一种方法。现在我唯一剩下的问题是:“如何使视图重新加载?”我已经确认了在按钮点击时SelectedContent会发生变化,但没有任何视觉反馈。 - Demiën Drost
你能检查一下ContentPresenter的绑定吗?看起来它没有指向正确的上下文,并且未能引用SelectedContent属性。启用调试设置中适当的WPF跟踪级别后,您可以检查输出窗口以查找绑定错误。视图是否显示内容?它总是空白的吗? - BionicCode
在“StudentView”中,我的绑定如下:<ContentPresenter Content="{Binding SelectedContent}"/>。这是伴随着一个代码后台的事件处理程序,仅用于检查代码是否正常运行。在此处,当调用“PropertyChanged”时,我只需执行一个控制台写入操作即可。这很有效。 - Demiën Drost
我应该补充一下,UserControl在加载时确实是可见的,我还尝试过在代码中交换usercontrols,这样做时我确实看到了我期望的视图。关于代码只是起作用,但我有一种感觉,即窗口必须以某种方式重新加载,即使WPF不应该这样工作。 - Demiën Drost
你的 PropertyChanged 调用器有一个拼写错误。你拼错了属性名。请更正。请查看我们聊天记录中的评论。 - BionicCode
显示剩余3条评论

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