web-dev-qa-db-ja.com

動的コンテンツのContentControlコンテンツのバインド

現在、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;

エラーはスローされませんが、「テスト」TextBlockは表示されません。

バインディングの概念が間違っている可能性が高いので、正しい方向にヒントを教えてください。

よろしく

27
Xaser

OK 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の2つのモデルを作成しました。両方のモデルには単一のプロパティのみがあり、必要に応じてプロパティを追加できます。

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にビューを提供する必要があります。つまり、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>

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にバインドされているプロパティです。ボタンをクリックしてこのプロパティを変更すると、メインウィンドウの画面が変わります。 MainWindowは、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 ApplicationタグのStartupUri="MainWindow.xaml"コードを削除して、起動時に2つのメインウィンドウが表示されないようにすることをお勧めします。

新しいプロジェクトにコピーして使用できるDelegateCommandおよびViewModelBaseクラスに注意してください。これは非常に単純な例です。 here および here からより良いアイデアを得ることができます

Editコメントでは、各ビューと関連する定型コードにクラスを持たなくてもよいかどうかを知りたいと思っていました。私の知る限り、答えはノーです。はい、1つの巨大なクラスを持つことができますが、各プロパティセッターに対してOnPropertyChangedを呼び出す必要があります。これにはかなりの欠点もあります。まず、結果のクラスを維持するのは本当に難しいでしょう。多くのコードと依存関係があります。第二に、DataTemplatesを使用してビューを「スワップ」することは困難です。 DataTemplatesでx:Keyを使用し、ユーザーコントロールでテンプレートバインディングをハードコーディングすることにより、引き続き可能です。本質的に、コードをそれほど短くするわけではありませんが、自分でコードを難しくすることになります。

あなたの主な不満は、モデルのプロパティをラップするために、ビューモデルに大量のコードを書く必要があると思います。 T4 templates をご覧ください。一部の開発者は、これを使用してボイラープレートコード(つまり、ViewModelクラス)を自動生成します。これを個人的に使用するのではなく、カスタムコードスニペットを使用して、viewmodelプロパティをすばやく生成します。

別のオプションは、PrismやMVVMLightなどのMVVMフレームワークを使用することです。私は自分で使用したことはありませんが、定型コードを簡単にする機能が組み込まれていると聞きました。

もう1つの注意点は、設定をデータベースに保存している場合、Entity FrameworkなどのORMフレームワークを使用してデータベースからモデルを生成できる可能性があることです。つまり、ビューモデルとビューを作成するだけです。

74