web-dev-qa-db-fra.com

Binding ContentControl Content pour le contenu dynamique

J'essaie actuellement d'obtenir la fonctionnalité d'un tabcontrol avec des onglets cachés en utilisant un ListView (sous forme d'onglets) et un ContentControl avec la liaison de la propriété de contenu.

J'ai lu un peu sur ce sujet et si j'ai bien compris, cela devrait fonctionner de cette façon:

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

Et dans le code derrière:

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;

Bien qu'il ne génère aucune erreur, il n'affiche pas le TextBlock "Test".

Il est probable que le concept de reliure soit erroné, donnez-moi un indice dans la bonne direction.

Cordialement

27
Xaser

Ok, j'ai créé un exemple simple pour vous montrer comment vous pouvez modifier dynamiquement le contenu de ContentControl en utilisant une approche MVVM (Model-View-ViewModel) avec liaison de données.

Je vous recommande de créer un nouveau projet et de charger ces fichiers pour voir comment tout cela fonctionne.

Nous devons d'abord implémenter l'interface INotifyPropertyChanged. Cela vous permettra de définir vos propres classes avec des propriétés qui avertiront l'interface utilisateur lorsqu'une modification des propriétés se produit. Nous créons une classe abstraite qui fournit cette fonctionnalité.

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

Nous devons maintenant disposer des modèles de données. Pour plus de simplicité, j'ai créé 2 modèles - HomePage et SettingsPage. Les deux modèles n'ont qu'une seule propriété, vous pouvez ajouter plus de propriétés si nécessaire.

HomePage.cs

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

SettingsPage.cs

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

Je crée ensuite des ViewModels correspondants pour envelopper chaque modèle. Notez que les viewmodels héritent de ma classe abstraite 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");
        }
    }
}

Nous devons maintenant fournir des vues pour chaque ViewModel. c'est-à-dire le HomePageView et le SettingsPageView. J'ai créé 2 UserControls pour cela.

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>

Nous devons maintenant définir le xaml pour MainWindow. J'ai inclus 2 boutons pour aider à naviguer entre les 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>

Nous avons également besoin d'un ViewModel pour la fenêtre principale. Mais avant cela, nous devons créer une autre classe afin de pouvoir lier nos boutons aux commandes.

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

Et maintenant, nous pouvons définir le MainWindowViewModel. CurrentViewModel est la propriété qui est liée à ContentControl sur MainWindow. Lorsque nous modifions cette propriété en cliquant sur les boutons, l'écran change sur la fenêtre principale. Le MainWindow sait quel écran (usercontrol) charger en raison des DataTemplates que j'ai définis dans la section 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."});
    }
}

Et enfin, nous devons remplacer le démarrage de l'application afin de pouvoir charger notre classe MainWindowViewModel dans la propriété DataContext de 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();
    }
}

Il serait également judicieux de supprimer le StartupUri="MainWindow.xaml" code dans la balise Application App.xaml afin que nous n'obtenions pas 2 MainWindows au démarrage.

Notez que les classes DelegateCommand et ViewModelBase qui peuvent simplement être copiées dans de nouveaux projets et utilisées. Ceci est juste un exemple très simple. Vous pouvez avoir une meilleure idée de ici et ici

Modifier Dans votre commentaire, vous vouliez savoir s'il était possible de ne pas avoir de classe pour chaque vue et code passe-partout connexe. Pour autant que je sache, la réponse est non. Oui, vous pouvez avoir une seule classe gigantesque, mais vous devrez toujours appeler OnPropertyChanged pour chaque ensemble de propriétés. Il y a également quelques inconvénients à cela. Premièrement, la classe résultante serait vraiment difficile à maintenir. Il y aurait beaucoup de code et de dépendances. Deuxièmement, il serait difficile d'utiliser des modèles de données pour "échanger" des vues. Il est toujours possible d'utiliser une clé x: dans vos DataTemplates et de coder en dur une liaison de modèle dans votre contrôle utilisateur. En substance, vous ne raccourcissez pas vraiment votre code, mais vous le rendrez plus difficile pour vous-même.

Je suppose que votre principal reproche est d'écrire autant de code dans votre modèle de vue pour envelopper les propriétés de votre modèle. Jetez un oeil à modèles T4 . Certains développeurs l'utilisent pour générer automatiquement leur code passe-partout (c'est-à-dire les classes ViewModel). Je ne l'utilise pas personnellement, j'utilise un extrait de code personnalisé pour générer rapidement une propriété viewmodel.

Une autre option serait d'utiliser un framework MVVM, tel que Prism ou MVVMLight. Je n'en ai pas utilisé moi-même, mais j'ai entendu que certains d'entre eux ont intégré des fonctionnalités pour rendre le code passe-partout facile.

Un autre point à noter est le suivant: si vous stockez vos paramètres dans une base de données, il peut être possible d'utiliser un cadre ORM comme Entity Framework pour générer vos modèles à partir de la base de données, ce qui signifie qu'il ne vous reste plus qu'à créer les modèles et les vues.

74
failedprogramming