Подтвердить что ты не робот

Связывание содержимого ContentControl для динамического содержимого

В настоящее время я пытаюсь реализовать функциональность tabcontrol со скрытыми вкладками, используя ListView (как вкладки) и 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;

Несмотря на то, что он не вызывает ошибок, он не отображает "Test" TextBlock.

Скорее всего, я понял концепцию привязки неправильно, пожалуйста, дайте мне подсказку в правильном направлении.

Привет

4b9b3361

Ответ 1

Хорошо, я выбил простой пример, чтобы показать вам, как вы можете динамически изменять содержимое ContentControl с помощью подхода MVVM (Model-View-ViewModel) с привязкой данных.

Я бы рекомендовал вам создать новый проект и загрузить эти файлы, чтобы увидеть, как все это работает.

Сначала нам нужно реализовать интерфейс INotifyPropertyChanged. Это позволит вам определить свои собственные классы со свойствами, которые будут уведомлять пользовательский интерфейс при изменении свойств. Мы создаем абстрактный класс, который предоставляет эту функциональность.

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

Затем я создаю соответствующие ViewModels для обертывания каждой модели. Обратите внимание, что 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");
        }
    }
}

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

Теперь нам нужно предоставить Views для каждого ViewModel. то есть HomePageView и SettingsPageView. Для этого я создал 2 UserControls.

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>

Теперь нам нужно определить xaml для MainWindow. Я включил 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>

Нам также нужна ViewModel для MainWindow. Но перед этим нам нужно создать еще один класс, чтобы мы могли привязать наши кнопки к командам.

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 - это свойство, связанное с ContentControl в MainWindow. Когда мы меняем это свойство, нажимая на кнопки, экран меняется на MainWindow. MainWindow знает, какой экран (usercontrol) загружается из-за DataTemplates, которые я определил в разделе Window.Resources.

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 в свойство DataContext 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();
    }
}

Также было бы неплохо удалить код StartupUri="MainWindow.xaml" в теге приложения App.xaml, чтобы мы не запускали 2 MainWindows при запуске.

Обратите внимание, что классы DelegateCommand и ViewModelBase, которые можно просто скопировать в новые проекты и использовать. Это просто очень простой пример. Вы можете получить лучшую идею от здесь и здесь

Edit В своем комментарии вы хотели знать, возможно ли иметь класс для каждого вида и связанного с ним шаблона. Насколько я знаю, ответ - нет. Да, у вас может быть один гигантский класс, но вам все равно нужно вызывать OnPropertyChanged для каждого свойства Setter. Также есть немало недостатков. Во-первых, результирующий класс будет очень трудно поддерживать. Было бы много кода и зависимостей. Во-вторых, было бы трудно использовать DataTemplates для "обмена" представлениями. Это все еще возможно с помощью x: Key в ваших DataTemplates и hardcoding привязки шаблона в вашем usercontrol. По сути, вы на самом деле не делаете ваш код намного короче, но вам будет сложнее для себя.

Я предполагаю, что ваша главная задача - написать так много кода в вашей модели просмотра, чтобы обернуть ваши свойства модели. Посмотрите T4 шаблоны. Некоторые разработчики используют это, чтобы автоматически генерировать свой шаблонный код (т.е. Классы ViewModel). Я не использую это лично, я использую собственный фрагмент кода для быстрого создания свойства viewmodel.

Другой вариант - использовать структуру MVVM, такую ​​как Prism или MVVMLight. Я не использовал его сам, но я слышал, что некоторые из них имеют встроенные функции, чтобы сделать шаблонный код легким.

Еще одно замечание: Если вы сохраняете свои настройки в базе данных, возможно, можно использовать инфраструктуру ORM, такую ​​как Entity Framework, для генерации ваших моделей из базы данных, а это означает, что все, что у вас осталось, создает представления и представления.