How to set dependency property of ItemsControl.Ite

2019-09-12 08:36发布

问题:

I have a main control (MainWindow.xaml) and an user control (ItemView.xaml). MainWindow contains an ItemsControl for all the ItemView-s and a simple button to add an item. All logic is (should be?) inside two corresponding viewmodels (MainWindowViewModel and ItemViewModel). Below is my code (made it as short as possible), but I have two problems with it:

  1. When a new item is added it is correctly displayed but the exception is raised (Cannot create default converter to perform 'two-way' conversions between types 'WpfApplication1.ItemView' and 'WpfApplication1.ItemViewModel'.).
  2. The OnDelete event handler in MainWindowViewModel is never raised? Edit: actually the ViewModel property inside BtnDeleteClick is null so yeah... of course.

Btw - I use Fody PropertyChanged.

MainWindow.xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpfApplication1="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Width="100" Height="35" Content="Add" HorizontalAlignment="Left" Margin="10" Click="BtnAddClick"></Button>
        <Border Grid.Row="1" MinHeight="50">
            <ItemsControl ItemsSource="{Binding ViewModel.Items}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <wpfApplication1:ItemView ViewModel="{Binding ., PresentationTraceSources.TraceLevel=High, Mode=TwoWay}"></wpfApplication1:ItemView>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Border>
    </Grid>
</Window>

MainWindow.xaml.cs:

[ImplementPropertyChanged]
public partial class MainWindow
{
    public MainWindowViewModel ViewModel { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        ViewModel = new MainWindowViewModel();
    }

    private void BtnAddClick(object sender, RoutedEventArgs e)
    {
        ViewModel.Add();
    }
}

MainWindowViewModel.cs:

[ImplementPropertyChanged]
public class MainWindowViewModel
{
    public ObservableCollection<ItemViewModel> Items { get; set; }

    public MainWindowViewModel()
    {
        Items = new ObservableCollection<ItemViewModel>();
    }

    public void Add()
    {
        var item = new ItemViewModel();
        item.OnDelete += (sender, args) =>
        {
            Debug.WriteLine("-- WAITING FOR THIS TO HAPPEN --");
            Items.Remove(item);
        };
        Items.Add(item);
    }
}

ItemViewModel.xaml:

<UserControl x:Class="WpfApplication1.ItemView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
            <Button Width="100" Height="35" Content="Delete" Click="BtnDeleteClick"></Button>
    </Grid>
</UserControl>

ItemView.xaml.cs:

[ImplementPropertyChanged]
public partial class ItemView
{
    public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register
    (
        "ViewModel", typeof(ItemViewModel), typeof(ItemView), new UIPropertyMetadata(null)
    );

    public ItemViewModel ViewModel
    {
        get { return (ItemViewModel)GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }

    public ItemView()
    {
        InitializeComponent();
    }

    private void BtnDeleteClick(object sender, RoutedEventArgs e)
    {
        ViewModel.Delete();
    }
}

And ItemViewModel.cs:

[ImplementPropertyChanged]
public class ItemViewModel
{
    public event EventHandler OnDelete;

    public void Delete()
    {
        var handler = OnDelete;
        if (handler != null)
        {
            handler(this, new EventArgs());
        }
    }
}

回答1:

You should not set

DataContext="{Binding RelativeSource={RelativeSource Self}}"

in the XAML of your ItemView. It effectively breaks the ViewModel="{Binding .}" binding in MainWindow.xaml, because the DataContext is no longer an ItemsViewModel, but an ItemsView.

As a rule, you should never explicitly set the DataContext of a UserControl, because all "external" bindings would then require an explicit Source or RelativeSource value.


That said, you're doing all this way too complicated. Instead of having a button click handler in your ItemsView, you could simply have a view model with a delete command, and bind the Button's Command property to this command.

It may look like this:

public class ItemViewModel
{
    public string Name { get; set; }
    public ICommand Delete { get; set; }
}

public class MainViewModel
{
    public MainViewModel()
    {
        Items = new ObservableCollection<ItemViewModel>();
    }

    public ObservableCollection<ItemViewModel> Items { get; private set; }

    public void AddItem(string name)
    {
        Items.Add(new ItemViewModel
        {
            Name = name,
            Delete = new DelegateCommand(p => Items.Remove(p as ItemViewModel))
        });
    }
}

and would be used like this:

<UserControl x:Class="WpfApplication1.ItemView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Button Content="Delete"
                Command="{Binding Delete}"
                CommandParameter="{Binding}"/>
    </Grid>
</UserControl>


标签: c# .net wpf xaml mvvm