为动态内容绑定 ContentControl Content

Posted

技术标签:

【中文标题】为动态内容绑定 ContentControl Content【英文标题】:Binding ContentControl Content for dynamic content 【发布时间】:2013-04-02 22:03:11 【问题描述】:

我目前正在尝试通过使用 ListView(作为选项卡)和绑定 Content 属性的 ContentControl 来实现具有隐藏选项卡的选项卡控件的功能。

我阅读了一些关于该主题的内容,如果我理解正确,它应该可以这样工作:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="20.0*"/>
        <ColumnDefinition Width="80.0*"/>
    </Grid.ColumnDefinitions>
    <ListBox Grid.Column="0">
        <ListBoxItem Content="Appearance"/>
    </ListBox>

    <ContentControl Content="Binding SettingsPage" Grid.Column="1"/>
</Grid>
.
.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ContentControl x:Key="AppearancePage">
        <TextBlock Text="Test" />
    </ContentControl>
    <ContentControl x:Key="AdvancedPage">
        <TextBlock Text="Test2" />
    </ContentControl>
</ResourceDictionary>

在后面的代码中:

public partial class MainWindow : MetroWindow
  
    private ContentControl SettingsPage;
    private ResourceDictionary SettingsPagesDict = new ResourceDictionary();

    public MainWindow()
    
        InitializeComponent();

        SettingsPagesDict.Source = new Uri("SettingsPages.xaml", UriKind.RelativeOrAbsolute);
        SettingsPage = SettingsPagesDict["AppearancePage"] as ContentControl;

尽管它不会引发错误,但它不会显示“测试”文本块。

我可能对绑定的概念有误解,请给我一个正确方向的提示。

问候

【问题讨论】:

ListView 在哪里?你能给我们更多的代码,比如超级更多。把你拥有的一切都给我们。 如果你想使用标签,为什么不使用 TabControl 控件呢?要隐藏/显示选项卡,您可以操作 TabItem 控件的 Visibility 属性(您可以在此处使用绑定)。另外,请阅读 Microsoft msdn.microsoft.com/en-us/library/ms752347.aspx 的数据绑定概述。我建议您不要绑定 UI 元素。在您的示例中,我将为 SettingsPage 创建一个类,该类将包含设置的多个属性。在 xaml 中,我将创建控件并绑定到每个属性。 @snowy gui Hedgehog:ListView 本身并不重要,它只是用来触发 changeditem 事件,我将在其中设置 ContentControl 的内容。基本上我的问题都是关于如何使用预定义的 ContentControl 模板从后面的代码中动态更改 ContentControl 的内容。 @failedprogramming 我尝试这样做的原因是这篇文章:link 这里。为什么不建议绑定 UI 元素? 你的问题是我+1的答案 【参考方案1】:

好的,我举了一个简单的例子,向您展示如何使用带有数据绑定的 MVVM(Model-View-ViewModel) 方法动态更改 ContentControl 的内容。

我建议您创建一个新项目并加载这些文件,看看它是如何工作的。

我们首先需要实现 INotifyPropertyChanged 接口。这将允许您定义您自己的具有属性的类,这些属性将在属性发生更改时通知 UI。我们创建一个提供此功能的抽象类。

ViewModelBase.cs

public abstract class ViewModelBase : INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    
        this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    
        var handler = this.PropertyChanged;
        if (handler != null)
        
            handler(this, e);
        
    

我们现在需要数据模型。为简单起见,我创建了 2 个模型 - HomePage 和 SettingsPage。两种模型只有一个属性,您可以根据需要添加更多属性。

HomePage.cs

public class HomePage

    public string PageTitle  get; set; 

SettingsPage.cs

public class SettingsPage

    public string PageTitle  get; set; 

然后我创建相应的 ViewModel 来包装每个模型。请注意,视图模型继承自我的 ViewModelBase 抽象类。

HomePageViewModel.cs

public class HomePageViewModel : ViewModelBase

    public HomePageViewModel(HomePage model)
    
        this.Model = model;
    

    public HomePage Model  get; private set; 

    public string PageTitle
    
        get
        
            return this.Model.PageTitle;
        
        set
        
            this.Model.PageTitle = value;
            this.OnPropertyChanged("PageTitle");
        
    

SettingsPageViewModel.cs

public class SettingsPageViewModel : ViewModelBase

    public SettingsPageViewModel(SettingsPage model)
    
        this.Model = model;
    

    public SettingsPage Model  get; private set; 

    public string PageTitle
    
        get
        
            return this.Model.PageTitle;
        
        set
        
            this.Model.PageTitle = value;
            this.OnPropertyChanged("PageTitle");
        
    

现在我们需要为每个 ViewModel 提供 View。即 HomePageView 和 SettingsPageView。我为此创建了 2 个用户控件。

HomePageView.xaml

<UserControl x:Class="WpfApplication3.HomePageView"
         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" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
        <TextBlock FontSize="20" Text="Binding Path=PageTitle" />
</Grid>

SettingsPageView.xaml

<UserControl x:Class="WpfApplication3.SettingsPageView"
         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" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<Grid>
    <TextBlock FontSize="20" Text="Binding Path=PageTitle" />
</Grid>

我们现在需要为 MainWindow 定义 xaml。我包含了 2 个按钮来帮助在 2 个“页面”之间导航。 MainWindow.xaml

<Window x:Class="WpfApplication3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:WpfApplication3"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <DataTemplate DataType="x:Type local:HomePageViewModel">
        <local:HomePageView />
    </DataTemplate>
    <DataTemplate DataType="x:Type local:SettingsPageViewModel">
        <local:SettingsPageView />
    </DataTemplate>
</Window.Resources>
<DockPanel>
    <StackPanel DockPanel.Dock="Left">
        <Button Content="Home Page" Command="Binding Path=LoadHomePageCommand" />
        <Button Content="Settings Page" Command="Binding Path=LoadSettingsPageCommand"/>
    </StackPanel>

    <ContentControl Content="Binding Path=CurrentViewModel"></ContentControl>
</DockPanel>

我们还需要一个用于 MainWindow 的 ViewModel。但在此之前,我们需要创建另一个类,以便我们可以将按钮绑定到命令。

DelegateCommand.cs

public class DelegateCommand : ICommand

    /// <summary>
    /// Action to be performed when this command is executed
    /// </summary>
    private Action<object> executionAction;

    /// <summary>
    /// Predicate to determine if the command is valid for execution
    /// </summary>
    private Predicate<object> canExecutePredicate;

    /// <summary>
    /// Initializes a new instance of the DelegateCommand class.
    /// The command will always be valid for execution.
    /// </summary>
    /// <param name="execute">The delegate to call on execution</param>
    public DelegateCommand(Action<object> execute)
        : this(execute, null)
    
    

    /// <summary>
    /// Initializes a new instance of the DelegateCommand class.
    /// </summary>
    /// <param name="execute">The delegate to call on execution</param>
    /// <param name="canExecute">The predicate to determine if command is valid for execution</param>
    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    
        if (execute == null)
        
            throw new ArgumentNullException("execute");
        

        this.executionAction = execute;
        this.canExecutePredicate = canExecute;
    

    /// <summary>
    /// Raised when CanExecute is changed
    /// </summary>
    public event EventHandler CanExecuteChanged
    
        add  CommandManager.RequerySuggested += value; 
        remove  CommandManager.RequerySuggested -= value; 
    

    /// <summary>
    /// Executes the delegate backing this DelegateCommand
    /// </summary>
    /// <param name="parameter">parameter to pass to predicate</param>
    /// <returns>True if command is valid for execution</returns>
    public bool CanExecute(object parameter)
    
        return this.canExecutePredicate == null ? true : this.canExecutePredicate(parameter);
    

    /// <summary>
    /// Executes the delegate backing this DelegateCommand
    /// </summary>
    /// <param name="parameter">parameter to pass to delegate</param>
    /// <exception cref="InvalidOperationException">Thrown if CanExecute returns false</exception>
    public void Execute(object parameter)
    
        if (!this.CanExecute(parameter))
        
            throw new InvalidOperationException("The command is not valid for execution, check the CanExecute method before attempting to execute.");
        
        this.executionAction(parameter);
    

现在我们可以定义 MainWindowViewModel。 CurrentViewModel 是绑定到 MainWindow 上 ContentControl 的属性。当我们通过单击按钮更改此属性时,主窗口上的屏幕会发生变化。由于我在 Window.Resources 部分中定义的 DataTemplate,MainWindow 知道要加载哪个屏幕(用户控件)。

MainWindowViewModel.cs

public class MainWindowViewModel : ViewModelBase

    public MainWindowViewModel()
    
        this.LoadHomePage();

        // Hook up Commands to associated methods
        this.LoadHomePageCommand = new DelegateCommand(o => this.LoadHomePage());
        this.LoadSettingsPageCommand = new DelegateCommand(o => this.LoadSettingsPage());
    

    public ICommand LoadHomePageCommand  get; private set; 
    public ICommand LoadSettingsPageCommand  get; private set; 

    // ViewModel that is currently bound to the ContentControl
    private ViewModelBase _currentViewModel;

    public ViewModelBase CurrentViewModel
    
        get  return _currentViewModel; 
        set
        
            _currentViewModel = value; 
            this.OnPropertyChanged("CurrentViewModel");
        
    

    private void LoadHomePage()
    
        CurrentViewModel = new HomePageViewModel(
            new HomePage()  PageTitle = "This is the Home Page.");
    

    private void LoadSettingsPage()
    
        CurrentViewModel = new SettingsPageViewModel(
            new SettingsPage()PageTitle = "This is the Settings Page.");
    

最后,我们需要重写应用程序启动,以便我们可以将 MainWindowViewModel 类加载到 MainWindow 的 DataContext 属性中。

App.xaml.cs

public partial class App : Application

    protected override void OnStartup(StartupEventArgs e)
    
        base.OnStartup(e);

        var window = new MainWindow()  DataContext = new MainWindowViewModel() ;
        window.Show();
    

删除 App.xaml 应用程序标记中的 StartupUri="MainWindow.xaml" 代码也是一个好主意,这样我们就不会在启动时获得 2 个 MainWindows。

请注意,DelegateCommand 和 ViewModelBase 类只能复制到新项目中并使用。 这只是一个非常简单的例子。您可以从here 和here 获得更好的想法

编辑 在您的评论中,您想知道是否可以不必为每个视图和相关的样板代码创建一个类。据我所知,答案是否定的。是的,您可以拥有一个巨大的类,但您仍然需要为每个 Property setter 调用 OnPropertyChanged。这也有不少缺点。首先,生成的类将很难维护。会有很多代码和依赖项。其次,很难使用 DataTemplates 来“交换”视图。仍然可以通过在 DataTemplates 中使用 x:Key 并在用户控件中硬编码模板绑定来实现。从本质上讲,你并没有真正让你的代码变得更短,但你会让自己变得更难。

我猜你的主要抱怨是不得不在你的视图模型中编写这么多代码来包装你的模型属性。看看T4 templates。一些开发人员使用它来自动生成他们的样板代码(即 ViewModel 类)。我个人不使用这个,我使用自定义代码sn-p快速生成viewmodel属性。

另一种选择是使用 MVVM 框架,例如 Prism 或 MVVMLight。我自己没有使用过,但我听说其中一些内置了使样板代码变得简单的功能。

还有一点需要注意的是: 如果您将设置存储在数据库中,则可以使用像实体框架这样的 ORM 框架从数据库生成模型,这意味着您剩下的就是创建视图模型和视图。

【讨论】:

不用担心。关于您关于过多代码的问题,请参阅我上面的编辑。 @Xaser 如果您在 *** 上发布您的问题,我更愿意。通过这种方式,您将能够获得更多帮助。您可以将任何新问题的链接发送给我,我会尽力提供帮助。谢谢 我不会真正称其为 gui 行为,它更多的是业务逻辑。上面显示的方法是 MVVM 的“ViewModel First”方法,这基本上意味着我们正在通过数据控制应用程序。我们根据需要创建和处理视图模型。实际的“gui 行为”发生在由数据模板定义的视图中。使用这种方法,您可以完全为相同的视图模型创建一个新的用户控件,并使用显示相同的 pagetitle 属性作为文本框而不是文本块,而无需更改视图模型代码。 You could easily hook up a different viewmodel in the resources if you wanted. 这就是困扰我的地方,如果我想要有 100 个视图,我必须将 100 个视图模型连接到 Resources 中的这 100 个视图。我想要的是&lt;DataTemplate DataType="x:Type vm:Binding currentViewModelType"&gt;&lt;v:Binding currentViewType&lt;/DataTemplate&gt;。你觉得我在这里吗? 对不起,我的无知,我还不太习惯 WPF 绑定。在这个例子中,我看不到&lt;ContentControl Content="Binding Path=CurrentViewModel"&gt; 如何知道要加载哪个.xaml 文件,因为它只是加载它的视图模型。在我的项目中,它显示字符串 MyProjc.MyApp.ViewModels.MyViewModel 而不是 XAML

以上是关于为动态内容绑定 ContentControl Content的主要内容,如果未能解决你的问题,请参考以下文章

将路径中的填充属性绑定到样式中 ContentControl 中的 Foreground 属性

xaml中的动态内容

当 ContentControl 的内容为空或为空时,在 ContentControl 中显示默认的 DataTemplate?

ContentControl 未与 TextBlock 中的文本对齐

[WPF源码分析]ContentControl依赖项属性的双向绑定,two-way binding view's DependencyProperty and ViewModel's

ContentControl 不显示内容