web-dev-qa-db-fra.com

Comment lier des commandes WPF entre un UserControl et une fenêtre parent

Je vais commencer par laisser une image parler.

MVVM User Control to Window wireframe

Donc, vous voyez, je veux créer un contrôle utilisateur WPF qui prend en charge la liaison au DataContext d'une fenêtre parent. Le contrôle utilisateur est simplement un Button et un ListBox avec un ItemTemplate personnalisé pour présenter des choses avec un Label et un Remove Button.

Le bouton Ajouter doit appeler un ICommand sur le modèle de vue principal pour interagir avec l'utilisateur dans la sélection d'une nouvelle chose (instance de IThing). Les boutons Supprimer dans le ListBoxItem dans le contrôle utilisateur doivent également appeler un ICommand sur le modèle de vue principal pour demander la suppression de l'élément lié. Pour que cela fonctionne, le bouton Supprimer devrait envoyer des informations d'identification au modèle de vue sur la chose demandant à être supprimée. Il existe donc 2 types de commandes qui devraient être liées à ce contrôle. Quelque chose comme AddThingCommand () et RemoveThingCommand (chose IThing).

J'ai fait fonctionner la fonctionnalité en utilisant les événements Click, mais cela semble hacky, produisant un tas de code derrière le XAML, et frotte contre le reste de l'implémentation MVVM vierge. Je veux vraiment utiliser normalement les commandes et MVVM.

Il y a suffisamment de code pour faire fonctionner une démo de base, je m'arrête de publier le tout pour réduire la confusion. Ce qui fonctionne qui me donne l'impression d'être si proche, c'est que le DataTemplate pour ListBox lie correctement le Label, et lorsque la fenêtre parent ajoute des éléments à la collection, ils apparaissent.

<Label Content="{Binding Path=DisplayName}" />

Bien que cela affiche correctement l'IThing, le bouton Supprimer juste à côté ne fait rien lorsque je clique dessus.

<Button Command="{Binding Path=RemoveItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">

Ce n'est pas vraiment inattendu puisque l'élément spécifique n'est pas fourni, mais le bouton Ajouter n'a rien à spécifier et il échoue également à appeler la commande.

<Button Command="{Binding Path=AddItemCommand, RelativeSource={RelativeSource AncestorType={x:Type userControlCommands:ItemManager }}}">

Donc, ce dont j'ai besoin est le correctif "de base" pour le bouton Ajouter, afin qu'il appelle la commande de la fenêtre parent pour ajouter une chose, et le correctif plus complexe pour le bouton Supprimer, afin qu'il appelle également la commande parent mais passe également sa chose liée.

Merci beaucoup pour vos idées,

22
Todd Sprang

C'est trivial, et rendu ainsi en traitant votre UserControl comme ce qu'il est - un contrôle (qui se trouve être composé à partir d'autres contrôles). Qu'est-ce que ça veut dire? Cela signifie que vous devez placer DependencyProperties sur votre UC à laquelle votre ViewModel peut se lier, comme tout autre contrôle. Les boutons exposent une propriété Command, les TextBoxes exposent une propriété Text, etc. Vous devez exposer, à la surface de votre UserControl, tout ce dont vous avez besoin pour qu'il fasse son travail.

Prenons un exemple trivial (jeté ensemble en moins de deux minutes). Je laisse de côté l'implémentation ICommand.

Tout d'abord, notre fenêtre

<Window x:Class="UCsAndICommands.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:t="clr-namespace:UCsAndICommands"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <t:ViewModel />
    </Window.DataContext>
    <t:ItemsEditor Items="{Binding Items}"
                   AddItem="{Binding AddItem}"
                   RemoveItem="{Binding RemoveItem}" />
</Window>

Notez que nous avons notre éditeur d'éléments, qui expose les propriétés de tout ce dont il a besoin - la liste des éléments qu'il modifie, une commande pour ajouter un nouvel élément et une commande pour supprimer un élément.

Ensuite, UserControl

<UserControl x:Class="UCsAndICommands.ItemsEditor"
             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"
             xmlns:t="clr-namespace:UCsAndICommands"
             x:Name="root">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type t:Item}">
            <StackPanel Orientation="Horizontal">
                <Button Command="{Binding RemoveItem, ElementName=root}"
                        CommandParameter="{Binding}">Remove</Button>
                <TextBox Text="{Binding Name}" Width="100"/>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <StackPanel>
        <Button Command="{Binding AddItem, ElementName=root}">Add</Button>
        <ItemsControl ItemsSource="{Binding Items, ElementName=root}" />
    </StackPanel>
</UserControl>

Nous lions nos contrôles aux DP définis à la surface de l'UC. S'il vous plaît, ne faites pas de bêtises comme DataContext=this; car cet anti-modèle rompt les implémentations UC plus complexes.

Voici les définitions de ces propriétés sur l'UC

public partial class ItemsEditor : UserControl
{
    #region Items
    public static readonly DependencyProperty ItemsProperty =
        DependencyProperty.Register(
            "Items",
            typeof(IEnumerable<Item>),
            typeof(ItemsEditor),
            new UIPropertyMetadata(null));
    public IEnumerable<Item> Items
    {
        get { return (IEnumerable<Item>)GetValue(ItemsProperty); }
        set { SetValue(ItemsProperty, value); }
    }
    #endregion  
    #region AddItem
    public static readonly DependencyProperty AddItemProperty =
        DependencyProperty.Register(
            "AddItem",
            typeof(ICommand),
            typeof(ItemsEditor),
            new UIPropertyMetadata(null));
    public ICommand AddItem
    {
        get { return (ICommand)GetValue(AddItemProperty); }
        set { SetValue(AddItemProperty, value); }
    }
    #endregion          
    #region RemoveItem
    public static readonly DependencyProperty RemoveItemProperty =
        DependencyProperty.Register(
            "RemoveItem",
            typeof(ICommand),
            typeof(ItemsEditor),
            new UIPropertyMetadata(null));
    public ICommand RemoveItem
    {
        get { return (ICommand)GetValue(RemoveItemProperty); }
        set { SetValue(RemoveItemProperty, value); }
    }        
    #endregion  
    public ItemsEditor()
    {
        InitializeComponent();
    }
}

Juste des DP sur la surface de l'UC. Pas de biggie. Et notre ViewModel est tout aussi simple

public class ViewModel
{
    public ObservableCollection<Item> Items { get; private set; }
    public ICommand AddItem { get; private set; }
    public ICommand RemoveItem { get; private set; }
    public ViewModel()
    {
        Items = new ObservableCollection<Item>();
        AddItem = new DelegatedCommand<object>(
            o => true, o => Items.Add(new Item()));
        RemoveItem = new DelegatedCommand<Item>(
            i => true, i => Items.Remove(i));
    }
}

Vous modifiez trois collections différentes, vous souhaiterez donc peut-être exposer plus de ICommands pour indiquer clairement que vous ajoutez/supprimez. Ou vous pouvez faire des économies et utiliser le CommandParameter pour le comprendre.

35
Will

Reportez-vous au code ci-dessous. UserControl.XAML

<Grid>
    <ListBox ItemsSource="{Binding Things}" x:Name="lst">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding ThingName}" Margin="3"/>
                    <Button Content="Remove" Margin="3" Command="{Binding ElementName=lst, Path=DataContext.RemoveCommand}" CommandParameter="{Binding}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Window.Xaml

<Window x:Class="MultiBind_Learning.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MultiBind_Learning"
    Title="Window1" Height="300" Width="300">
<StackPanel Orientation="Horizontal">
    <Button Content="Add" Width="50" Height="25" Command="{Binding AddCommnd }"/>
    <local:UserControl2/>
</StackPanel>

Window.xaml.cs

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        this.DataContext = new ThingViewModel();
    }
}

ThingViewModel.cs

  class ThingViewModel
{
    private ObservableCollection<Thing> things = new ObservableCollection<Thing>();

    public ObservableCollection<Thing> Things
    {
        get { return things; }
        set { things = value; }
    }

    public ICommand AddCommnd { get; set; }
    public ICommand RemoveCommand { get; set; }

    public ThingViewModel()
    {
        for (int i = 0; i < 10; i++)
        {
            things.Add(new Thing() { ThingName="Thing" +i});
        }

        AddCommnd = new BaseCommand(Add);
        RemoveCommand = new BaseCommand(Remove);
    }

    void Add(object obj)
    {
      things.Add(new Thing() {ThingName="Added New" });
    }

    void Remove(object obj)
    {
      things.Remove((Thing)obj);
    }
}

Thing.cs

class Thing :INotifyPropertyChanged
{
    private string thingName;

    public string ThingName
    {
        get { return thingName; }
        set { thingName = value; OnPropertyChanged("ThingName"); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }
}

BaseCommand.cs

public class BaseCommand : ICommand
{
    private Predicate<object> _canExecute;
    private Action<object> _method;
    public event EventHandler CanExecuteChanged;

    public BaseCommand(Action<object> method)
    {
        _method = method;            
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        _method.Invoke(parameter);
    }
}

Au lieu de la commande Base, vous pouvez essayer RelayCommand de MVVMLight ou DelegateCommand des bibliothèques PRISM.

4

Par défaut, votre contrôle utilisateur héritera du DataContext de son conteneur. Ainsi, la classe ViewModel que votre fenêtre utilise peut être liée directement par le contrôle utilisateur, en utilisant la notation Binding en XAML. Il n'est pas nécessaire de spécifier DependentProperties ou RoutedEvents, il suffit de se lier aux propriétés de la commande comme d'habitude.

2
Rye bread