如何在 UserControl 中显示基于层次结构的对象并使用 MVVM 呈现命令?

How can I display hierarchical-based objects in an UserControl and present Commands using MVVM?

[由于这个问题是关于 MVVM 思想的,所以我在这个问题中使用了伪代码和 -XAML 以使其尽可能简短和准确。]

我最近 运行 遇到了一个 MVVM 问题,我无法使用最佳实践建议解决该问题。
想象一下,您正在尝试为某物创建一个编辑器程序。我们将使用图书馆模型(我想不出更好的模型):

UI可能是这样的可能设计的不好,毕竟这只是一个例子:

现在我想出了这个 MainWindow 定义:

<Window DataContext="{PseudoResource EditorVM}">
    <DockPanel>
        <controls:AllBooksControl Books="{Binding Books}"
                                  AddCommand="{Binding AddBookCommand}"
                                  SelectCommand="{Binding SelectBookCommand}"
                                  DockPanel.Dock="Left" />
        <controls:EditBooksControl Books="{Binding SelectedBooks}"
                                   DeleteBookCommand="{Binding DeleteBookCommand}"
                                   AddChapterCommand="{Binding AddChapterCommand}"
                                   DeleteChapterCommand="{Binding DeleteChapterCommand}"
                                   AddParagraphCommand="{Binding AddParagraphCommand}"
                                   DeleteParagraphCommand="{Binding DeleteParagraphCommand}" />
    </DockPanel>
</Window>

虽然这看起来仍然很整洁,但我似乎无法在 UserControl 本身中实现所需的行为:

<UserControl x:Class="EditBooksControl" x:Name="root">
    <UserControl.Resources>
        <DataTemplate DataType="Book">
            <StackPanel Orientation="Vertical">
                <Button Content="REMOVE"
                        Command="{Binding DeleteBookCommand, ElementName=root}"
                        CommandParameter="{Binding}" />
                <TextBox Content="{Binding Title}" />
                <WrapPanel ItemsSource="{Binding Chapters}" (Ignoring the additional Add tile here) />
            </StackPanel>
        </DataTemplate>
        <DataTemplate DataType="Chapter" (Template for the chapter tiles)>
            <StackPanel Orientation="Vertical">
                <Button Content="REMOVE"
                        Command="{Binding DeleteChapterCommand, ElementName=root}"
                        CommandParameter="{Binding}"
                        CommandParameter2="... I need to pass the chapter's parent book here, but there's no such a second command parameter, too ..." />
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    <TabControl ItemsSource="{Binding Books, ElementName=root}" />
</UserControl>

当我沿着本书的层次结构树走下去时,事情开始变得复杂起来。例如,删除一个段落时,我必须将三个命令参数传递给 MainWindow(在哪本书中?在哪一章中?在哪一段中?)。

我可以通过摆脱 UserControlDependencyProperties、将 TabControl 直接放在 MainWindow 并添加单独的 ViewModels 到子控件。这样 EditBookControl 可以自行进行所需的更改:

(Everything in MainWindow)

public List<Control> EditControls;
<TabControl ItemsSource="{Binding EditControls}" />
SelectBookCommand_Executed { EditControls.Add(new EditBookControl(new BookVM(e.CommandParameter as Book))); }

据我所知,这不是要走的路;最佳做法是使用一个 ViewModel 每个 Window 如下所述:

老实说,我无法想象每个 Window 只允许一个 ViewModel。 Visual Studio 也是使用 WPF 编写的 - 他们真的使用一个 ViewModel 来处理大量功能吗?
我想知道如何解决这个难题编写干净漂亮的代码。

一旦你理解了 MVVM 就非常简单。

您需要知道的一件事是,在 DataTemplate 中,DataContext 是我们在其上应用 DataTemplate 的对象。

所以在LibraryV.xaml的ItemsControl中,我们将一个DataTemplate应用于BookVM的集合,所以BookV中的DataContext就是相关的BookVM。

它让你很容易获得你想要的信息。

你的问题的一个非常简单(不完整)的版本:

LibraryVM.cs:

public class LibraryVM{

    public LibraryVM(LibraryModel model) {
        _model = model
    }

    #region CmdRemove
    private DelegateCommand _cmdRemove;
    public DelegateCommand CmdRemove {
        get { return _cmdRemove ?? (_cmdRemove = new DelegateCommand(Remove, CanRemove)); }
    }

    private void Remove(Object parameter) {
        BookVM bookToRemove = (BookVM)parameter;
        Books.Remove(bookToRemove);
    }

    private void CanRemove(Object parameter) {
        BookVM bookToRemove = parameter as BookVM;
        return bookToRemove != null && Books.Contains(bookToRemove);
    }
    #endregion

    private readonly LibraryModel _model;

    public List<BookVM> Books {get {return _model.Books.Select(b => new BookVM(b)).ToList();}}
}

BookVM.cs:

public class BookVM{

    public LibraryVM(BookModel model) {
        _model = model
    }

    private readonly BookModel _model;

    public String Title {get {return _model.Title;}}

    public List<ChapterVM> Chapters {get {return _model.Chapters.Select(c => new ChapterVM(c)).ToList();}} 
}

BookV.xaml:

<UserControl ...>
    <StackPanel Orientation="Vertical">
        <Button><!-- Button to remove the book from the library -->
            <TextBlock Text="Remove" 
                        Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type view:LibraryV}}, Path=DataContext.CmdRemove}" 
                        CommandParameter="{Binding Mode=OneTime}"/>
        </Button>
        <TextBlock Text="{Binding Title, Mode=OneWay}"/>
        <ItemsControl ItemsSource="{Binding Chapters, Mode=OneWay}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:ChapterVM}">
                    <view:ChapterV/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>

</UserControl>

LibraryV.xaml:

<UserControl ...>
    <StackPanel Orientation="Vertical">
        <ItemsControl ItemsSource="{Binding Books, Mode=OneWay}">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:BookVM}">
                    <view:BookV/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</UserControl>

您唯一需要设置的 DataContext 是 LibraryV 的 DataContext。