绑定ContentControl内容以实现动态内容

34

我目前正在尝试使用ListView(作为选项卡)和ContentControl来绑定Content属性,以实现隐藏选项卡的TabControl功能。

我在这个主题上有点了解,如果我没理解错的话,应该是这样工作的:

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

尽管它没有抛出错误,但它不显示“Test”文本块。

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

敬礼


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

93

好的,我已经为您制作了一个简单的示例,以展示您如何使用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);
        }
    }
}

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

HomePage.cs

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

设置页面.cs

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

我随后创建了对应的ViewModel来封装每个模型。请注意,ViewModels继承自我的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");
        }
    }
}

设置页面视图模型.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提供视图,即HomePageView和SettingsPageView。我为此创建了2个UserControl。

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>

设置页面视图.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。我添加了两个按钮,以帮助在这两个“页面”之间导航。 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的属性。当我们通过点击按钮更改此属性时,MainWindow上的屏幕会发生变化。MainWindow知道要加载哪个屏幕(usercontrol),因为我在Window.Resources部分中定义了DataTemplates。

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"代码是个好主意,这样我们就不会在启动时得到两个MainWindows。

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

编辑 在您的评论中,您想知道是否有可能不必为每个视图和相关的样板文件代码编写一个类。据我所知,答案是否定的。是的,你可以有一个单独的巨大的类,但你仍然需要为每个属性设置器调用OnPropertyChanged。这也有相当多的缺点。首先,产生的类将非常难以维护。那里会有很多代码和依赖关系。其次,使用DataTemplates来“切换”视图将很难。通过在您的DataTemplates中使用x:Key并在usercontrol中硬编码一个模板绑定,这仍然是可能的。实质上,你并没有真正缩短你的代码,但你会让自己更加困难。

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

另一个选项是使用MVVM框架,如Prism或MVVMLight。我没有用过其中任何一个,但我听说它们中的一些已经内置了使样板代码变得容易的功能。

另一个要注意的问题是: 如果您将设置存储在数据库中,可能可以使用ORM框架(如Entity Framework)从数据库生成模型,这意味着您只需要创建viewmodels和views。


2
没问题。请看我上面关于你对过多代码的问题的编辑。 - failedprogramming
2
我不会真的称其为GUI行为,它更多是业务逻辑。上面展示的方法是MVVM中“ViewModel First”方法的一种,这基本上意味着我们通过数据来控制应用程序。我们根据需要创建和处理视图模型。实际的“GUI行为”发生在视图上,由数据模板定义。使用这种方法,您完全可以为相同的视图模型创建一个新的用户控件,并将相同的PageTitle属性显示为文本框而不是文本块,而无需更改视图模型代码。 - failedprogramming
2
如果你想要在资源中轻松地连接不同的视图模型,那是很容易的。但这里困扰我的是,如果我想要有100个视图,我就必须在“资源”中连接100个视图模型到这些100个视图上。我想要的是类似于<DataTemplate DataType="{x:Type vm:{Binding currentViewModelType}}"><v:{Binding currentViewType}</DataTemplate>这样的东西。你能理解我的意思吗? - QuantumHive
1
抱歉,我对WPF绑定还不太熟悉。在这个例子中,我无法理解<ContentControl Content="{Binding Path=CurrentViewModel}">是如何知道要加载哪个.xaml文件的,因为它只加载了它的视图模型。在我的项目中,它显示的是字符串MyProjc.MyApp.ViewModels.MyViewModel,而不是XAML文件。 - Christopher Francisco
1
@ChristopherFrancisco,请查看MainWindow资源部分中的DataTemplate规范。 - failedprogramming
显示剩余7条评论

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