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

Как связать команды WPF между UserControl и родительским окном

Начну с того, что на снимке можно поговорить.

MVVM User Control to Window wireframe

Итак, вы видите, я хочу создать пользовательский элемент управления WPF, который поддерживает привязку к родительскому окну DataContext. Пользовательский элемент управления - это просто Button и ListBox с пользовательским ItemTemplate для представления вещей с помощью метки и кнопки удаления.

Кнопка "Добавить" должна вызвать ICommand в основной модели представления, чтобы взаимодействовать с пользователем при выборе новой вещи (экземпляр IThing). Кнопки "Удалить" в элементе ListBoxItem в пользовательском элементе управления также должны вызвать ICommand в основной модели представления, чтобы запросить удаление связанной вещи. Чтобы это сработало, кнопка "Удалить" должна была отправить некоторую идентифицирующую информацию в модель представления о том, что требуется удалить. Таким образом, существует два типа команд, которые должны быть привязаны к этому элементу управления. Что-то вроде AddThingCommand() и RemoveThingCommand (вещь IThing).

Я получил функциональность, работающую с использованием событий Click, но которая кажется взломанной, создавая кучу кода за XAML и протирающая остальную часть первоначальной реализации MVVM. Я действительно хочу использовать Commands и MVVM в обычном режиме.

Там достаточно кода, чтобы получить базовую демонстрационную работу, я держусь за публикацию всего, чтобы уменьшить путаницу. Что работает, что заставляет меня чувствовать, что я настолько близок, что DataTemplate для ListBox правильно привязывает метку, и когда родительское окно добавляет элементы в коллекцию, они появляются.

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

Пока это правильно отображает IThing, кнопка "Удалить" рядом с ним ничего не делает, когда я нажимаю на нее.

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

Это не является неожиданным, поскольку конкретный элемент не указан, но кнопка "Добавить" не должна указывать что-либо, а также не может вызвать команду.

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

Итак, мне нужно "базовое" исправление для кнопки "Добавить" , чтобы он вызывал команду родительского окна, чтобы добавить вещь, и более сложное исправление для кнопки "Удалить", так что она также вызывает родительскую команду, но также проходит вдоль своей связанной вещи.

Большое спасибо за любые идеи,

4b9b3361

Ответ 1

Это тривиально и сделано так, рассматривая ваш UserControl как то, что он есть - элемент управления (который просто происходит из других элементов управления). Что это значит? Это означает, что вы должны размещать DependencyProperties в своем UC, с которым может связываться ViewModel, как и любой другой элемент управления. Кнопки отображают свойство Command, TextBoxes выставляют свойство Text и т.д. Вам нужно выставить на поверхности вашего UserControl все необходимое для выполнения своей работы.

Возьмем тривиальный пример (за два минуты). Я не буду выполнять реализацию ICommand.

Во-первых, наше окно

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

Обратите внимание, что у нас есть наш редактор Items, который предоставляет свойства для всего, что ему нужно - список элементов, которые он редактирует, команду для добавления нового элемента и команду для удаления элемента.

Далее, 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>

Мы привязываем наши элементы управления к DP, определенным на поверхности UC. Пожалуйста, не делайте никаких глупостей, таких как DataContext=this;, поскольку этот анти-шаблон нарушает более сложные реализации UC.

Здесь определения этих свойств на 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();
    }
}

Просто DP на поверхности UC. Нет, biggie. И наша ViewModel так же проста

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

Вы редактируете три разных коллекции, поэтому вам может потребоваться показать больше ICommands, чтобы они поняли, что вы добавляете/удаляете. Или вы можете дешево и использовать CommandParameter, чтобы понять это.

Ответ 2

Обратитесь к приведенному ниже коду. 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);
    }
}

Вместо команды Base вы можете попробовать RelayCommand из MVVMLight или DelegateCommand из библиотек PRISM.

Ответ 3

По умолчанию ваш пользовательский элемент наследует DataContext своего контейнера. Таким образом, класс ViewModel, который использует ваше окно, может быть привязан непосредственно пользовательским элементом управления, используя нотацию привязки в XAML. Нет необходимости указывать DependentProperties или RoutedEvents, просто привязывайте к свойствам команды как обычно.