为动态内容绑定 ContentControl 内容 [英] Binding ContentControl Content for dynamic content

查看:41
本文介绍了为动态内容绑定 ContentControl 内容的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我阅读了一些关于该主题的内容,如果我做对了,它应该是这样工作的:

<Grid.ColumnDefinitions><ColumnDefinition Width="20.0*"/><ColumnDefinition Width="80.0*"/></Grid.ColumnDefinitions><ListBox Grid.Column="0"><ListBoxItem Content="外观"/></列表框><ContentControl Content="{Binding SettingsPage}" Grid.Column="1"/></网格>..<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="测试"/></内容控制><ContentControl x:Key="AdvancedPage"><TextBlock Text="Test2"/></内容控制></ResourceDictionary>

在后面的代码中:

公共部分类 MainWindow : MetroWindow{私人内容控制设置页面;私有 ResourceDictionary SettingsPagesDict = 新 ResourceDictionary();公共主窗口(){初始化组件();SettingsPagesDict.Source = new Uri("SettingsPages.xaml", UriKind.RelativeOrAbsolute);SettingsPage = SettingsPagesDict["AppearancePage"] as ContentControl;

虽然它没有抛出任何错误,但它不显示测试"文本块.

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

问候

解决方案

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

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

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

ViewModelBase.cs

公共抽象类 ViewModelBase : INotifyPropertyChanged{公共事件 PropertyChangedEventHandler PropertyChanged;protected void OnPropertyChanged(string propertyName){this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));}protected virtual void OnPropertyChanged(PropertyChangedEventArgs e){var handler = this.PropertyChanged;如果(处理程序!= null){处理程序(这个,e);}}}

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

HomePage.cs

公共类主页{公共字符串 PageTitle { 获取;放;}}

SettingsPage.cs

公共类设置页面{公共字符串 PageTitle { 获取;放;}}

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

HomePageViewModel.cs

公共类 HomePageViewModel : ViewModelBase{公共主页视图模型(主页模型){this.Model = 模型;}公共主页模型 { get;私人订制;}公共字符串 PageTitle{得到{返回 this.Model.PageTitle;}放{this.Model.PageTitle = 值;this.OnPropertyChanged("PageTitle");}}}

SettingsPageViewModel.cs

公共类 SettingsPageViewModel : ViewModelBase{公共设置页面视图模型(设置页面模型){this.Model = 模型;}公共设置页面模型 { 获取;私人订制;}公共字符串 PageTitle{得到{返回 this.Model.PageTitle;}放{this.Model.PageTitle = 值;this.OnPropertyChanged("PageTitle");}}}

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

HomePageView.xaml

<网格><TextBlock FontSize="20" Text="{Binding Path=PageTitle}"/></网格>

SettingsPageView.xaml

<网格><TextBlock FontSize="20" Text="{Binding Path=PageTitle}"/></网格>

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

<窗口.资源><DataTemplate DataType="{x:Type local:HomePageViewModel}"><local:HomePageView/></数据模板><DataTemplate DataType="{x:Type local:SettingsPageViewModel}"><local:SettingsPageView/></数据模板></Window.Resources><DockPanel><StackPanel DockPanel.Dock="左"><Button Content="Home Page" Command="{绑定路径=LoadHomePageCommand}"/><Button Content="Settings Page" Command="{绑定路径=LoadSettingsPageCommand}"/></StackPanel><ContentControl Content="{Binding Path=CurrentViewModel}"></ContentControl></DockPanel>

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

DelegateCommand.cs

公共类 DelegateCommand : ICommand{///<总结>///执行此命令时要执行的操作///</总结>私有动作<对象>执行动作;///<总结>///判断命令是否有效执行的谓词///</总结>私有谓词<对象>canExecutePredicate;///<总结>///初始化 DelegateCommand 类的新实例.///该命令将始终有效执行.///</总结>///<param name="execute">调用执行的委托</param>公共委托命令(操作<对象>执行):这个(执行,空){}///<总结>///初始化 DelegateCommand 类的新实例.///</总结>///<param name="execute">调用执行的委托</param>///<param name="canExecute">判断命令是否有效执行的谓词</param>public DelegateCommand(Action<object> execute, Predicate<object> canExecute){如果(执行 == 空){throw new ArgumentNullException("execute");}this.executionAction = 执行;this.canExecutePredicate = canExecute;}///<总结>///当 CanExecute 改变时引发///</总结>公共事件 EventHandler CanExecuteChanged{添加 { CommandManager.RequerySuggested += value;}删除 { CommandManager.RequerySuggested -= value;}}///<总结>///执行支持此 DelegateCommand 的委托///</总结>///<param name="parameter">传递给谓词的参数</param>///<returns>如果命令对执行有效</returns>Truepublic bool CanExecute(对象参数){返回 this.canExecutePredicate == null ?真:this.canExecutePredicate(参数);}///<总结>///执行支持此 DelegateCommand 的委托///</总结>///<param name="parameter">传递给委托的参数</param>///<exception cref="InvalidOperationException">如果 CanExecute 返回 false 则抛出</exception>公共无效执行(对象参数){if (!this.CanExecute(parameter)){throw new InvalidOperationException("命令无效,执行前检查CanExecute方法.");}this.executionAction(参数);}}

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

MainWindowViewModel.cs

公共类 MainWindowViewModel : ViewModelBase{公共 MainWindowViewModel(){this.LoadHomePage();//将命令连接到相关方法this.LoadHomePageCommand = new DelegateCommand(o => this.LoadHomePage());this.LoadSettingsPageCommand = new DelegateCommand(o => this.LoadSettingsPage());}公共 ICommand LoadHomePageCommand { 获取;私人订制;}公共 ICommand LoadSettingsPageCommand { 获取;私人订制;}//当前绑定到 ContentControl 的 ViewModel私有 ViewModelBase _currentViewModel;公共 ViewModelBase CurrentViewModel{得到 { 返回 _currentViewModel;}放{_currentViewModel = 值;this.OnPropertyChanged("CurrentViewModel");}}私有无效 LoadHomePage(){CurrentViewModel = 新的 HomePageViewModel(new HomePage() { PageTitle = "这是主页."});}私有无效 LoadSettingsPage(){CurrentViewModel = 新的 SettingsPageViewModel(new SettingsPage(){PageTitle = "这是设置页面."});}}

最后,我们需要覆盖应用程序启动,以便我们可以将 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 类只能复制到新项目中使用.这只是一个非常简单的例子.你可以从这里这里

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

我猜您的主要抱怨是必须在您的视图模型中编写如此多的代码来包装您的模型属性.查看 T4 模板.一些开发人员使用它来自动生成他们的样板代码(即 ViewModel 类).我个人不使用它,我使用自定义代码片段来快速生成一个视图模型属性.

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

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

I'm currently trying to achieve the functionality of a tabcontrol with hidden tabs by using a ListView (as tabs) and a ContentControl with Binding the Content Property.

I read a bit on that topic and if I got it right, it should work this way:

<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>

And in the code behind:

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;

Allthough it throws no error, it doesn't display the "Test" TextBlock.

Its likely I got the concept of binding wrong, please give me a hint in the right direction.

Regards

解决方案

Ok I've knocked up a simple example to show you how you can dynamically change the content of the ContentControl using a MVVM(Model-View-ViewModel) approach with data binding.

I would recommend that you create a new project and load these files to see how it all works.

We first need to implement INotifyPropertyChanged interface. This will allow you to define your own classes with properties that will notify the UI when a change to the properties occurs. We create an abstract class that provides this functionality.

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);
        }
    }
}

We now need to have the data models. For simplicity, I've created 2 models - HomePage and SettingsPage. Both models only have a single property, you can add more properties as required.

HomePage.cs

public class HomePage
{
    public string PageTitle { get; set; }
}

SettingsPage.cs

public class SettingsPage
{
    public string PageTitle { get; set; }
}

I then create corresponding ViewModels to wrap each model. Note that the viewmodels inherit from my ViewModelBase abstract class.

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");
        }
    }
}

Now we need to provide Views for each ViewModel. i.e. The HomePageView and the SettingsPageView. I created 2 UserControls for this.

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>

We now need to define the xaml for the MainWindow. I have included 2 buttons to help navigate between the 2 "pages". 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>

We also need a ViewModel for the MainWindow. But before that we need create another class so that we can bind our Buttons to Commands.

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);
    }
}

And now we can defind the MainWindowViewModel. CurrentViewModel is the property that is bound to the ContentControl on the MainWindow. When we change this property by clicking on the buttons, the screen changes on the MainWindow. The MainWindow knows which screen(usercontrol) to load because of the DataTemplates that I have defined in the Window.Resources section.

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."});
    }
}

And finally, we need to override the application startup so that we can load our MainWindowViewModel class into the DataContext property of the MainWindow.

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();
    }
}

It would also be a good idea to remove the StartupUri="MainWindow.xaml" code in the App.xaml Application tag so that we don't get 2 MainWindows on start up.

Note that the DelegateCommand and ViewModelBase classes that just can be copied into new projects and used. This is just a very simple example. You can get a better idea from here and here

Edit In your comment, you wanted to know if it is possible to not have to have a class for each view and related boilerplate code. As far as I know, the answer is no. Yes, you can have a single gigantic class, but you would still need to call OnPropertyChanged for each Property setter. There are also quite a few drawbacks to this. Firstly, the resulting class would be really hard to maintain. There would be a lot of code and dependencies. Secondly, it would be hard to use DataTemplates to "swap" views. It is still possible by using a x:Key in your DataTemplates and hardcoding a template binding in your usercontrol. In essence, you aren't really making your code much shorter, but you will be making it harder for yourself.

I'm guessing your main gripe is having to write so much code in your viewmodel to wrap your model properties. Have a look at T4 templates. Some developers use this to auto-generate their boilerplate code (i.e. ViewModel classes). I don't use this personally, I use a custom code snippet to quickly generate a viewmodel property.

Another option would be to use a MVVM framework, such as Prism or MVVMLight. I haven't used one myself, but I've heard some of them have built in features to make boilerplate code easy.

Another point to note is: If you are storing your settings in a database, it might be possible to use an ORM framework like Entity Framework to generate your models from the database, which means all you have left is creating the viewmodels and views.

这篇关于为动态内容绑定 ContentControl 内容的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆