0

[As this question is about MVVM thoughts, I'm using pseudo-code and -XAML in this question to get it as short and precise as possible.]

I've recently ran into a MVVM problem that I wasn't able to solve yet using best practice recommendations.
Imagine you're trying to create an editor program for something. We're going to use a library model (I couldn't come up with a better one):

  • A library contains one or many books
  • A book contains one or more chapters
  • A chapter contains one or more paragraphs

The UI might look like this. It might be designed badly, but this is just an example after all:

Editor window example

Now I came up with this MainWindow definition:

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

While this still looks neat, I can't seem to implement the required behaviour in the UserControl itself:

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

Things start to get complicated as I walk down the Book's hierarchical tree. For example I'd have to pass three command parameters to the MainWindow when deleting a paragraph (in which book?, in which chapter?, which paragraph?).

I was able to solve all of this by getting rid of the UserControl's DependencyProperties, placing the TabControl directly in the MainWindow and adding separate ViewModels to the child controls. This way the EditBookControl can make the required changes by itself:

(Everything in MainWindow)

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

As I read, this is not the way to go; best practice is using one ViewModel per Window as described here:

I honestly can't imagine that only one ViewModel per Window is allowed. Visual Studio is written by using WPF as well - did they really used one ViewModel for the tons and tons of features?
I'd like to know how I can solve this dilemma and writing clean and nice code.

Community
  • 1
  • 1
Physikbuddha
  • 1,652
  • 1
  • 15
  • 30
  • One view = one ViewModel class. If you don't this get really complex really quick (DataContext nightmare). Define three viewmodel that contain a list (list of books for library, list of chapters for a book... you already said it and have done the design! Why not using it?) and three corresponding xaml containing an ItemsControl and an ItemTemplate that is the subxaml (for example the ItemTemplate inside the Library.xaml's ItemsControl will be composed of Book.xaml) – nkoniishvt Jun 28 '16 at 11:35
  • @nkoniishvt I appreciate your comment, but this doesn't answer all my questions. If I really should provide my `UserControl`s with `ViewModel`s, how should I implement that? Should the `UC` 1) accept the `VM` as a `DependencyProperty`? 2) accept a `VM` in it's constructor (I won't be able to create the `UserControl` in XAML anymore)? 3) create a default `VM` by itself? How would be the empty `VM` get access to the `UC`'s Book when this is stored in a `DependencyProperty`? It would really help if you can expand that comment to an actual answer with XAML examples to help me grab the concepts. – Physikbuddha Jun 28 '16 at 11:48
  • Answered your post, it's way easier than all your ideas using DataTemplate – nkoniishvt Jun 28 '16 at 12:05

1 Answers1

1

MVVM is very easy once you understand it.

One thing you need to know is that in a DataTemplate the DataContext is the object on which we apply the DataTemplate.

So In the LibraryV.xaml's ItemsControl, we apply a DataTemplate to a collection of BookVM, so the DataContext in the BookV is the related BookVM.

It makes it really easy to get information you want.

A very simple (not complete) version of your problem:

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>

The only DataContext you need to set is the LibraryV's DataContext here.

nkoniishvt
  • 2,442
  • 1
  • 14
  • 29
  • Ahh, well, I see - I create DataTemplates based on the ViewModel rather than on the Model itself. Let me think and meditate through this on my way home. – Physikbuddha Jun 28 '16 at 12:07
  • View layer interact with ViewModel layer in MVVM, Model store data, ViewModel pick what the View layer want and make it available to View layer. Note that this is a simple example to illustrate how DataContext "automatically" flows down the tree, you'll probably need to implement INotifyPropertyChanged sooner or later and use something else than a linq query in the lists getter. – nkoniishvt Jun 28 '16 at 12:09
  • @nkoniiishvt: `Select(b => new ChapterVM(v))` is probably a typo (`b` vs. `v`). – mechanic Jun 28 '16 at 15:20
  • @nkoniishvt How would you set the command's Bindings in XAML? For instance, the BookV has a REMOVE command that needs to be processed in the LibraryVM. – Physikbuddha Jun 28 '16 at 20:16
  • @Physikbuddha edited the post to add a Command that removes a BookVM from a LibraryVM – nkoniishvt Jun 29 '16 at 05:46
  • So, using the FindAncestor solution isn't a bad thing as I thought. I got my application working now, thanks for your efforts! – Physikbuddha Jun 29 '16 at 09:52
  • @Physikbuddha nothing wrong with FindAncestor in MVVM. You could also have the BookVM raise an event to notify the LibraryVM to delete it – nkoniishvt Jun 29 '16 at 10:05