MVVM и иерархия View/ViewModel

Я работаю над своей первой игрой, использующей С# и XAML для Windows 8. Я все еще изучаю основные концепции и лучшие практики, и MVVM является препятствием. Я попытаюсь задать вопрос в двух частях.

Фон

Игра, которую я делаю, это судоку. Судоку имеет доску, которая содержит сетку из 9x9 плиток. У меня три модели - Game, Board и Tile. Когда создается Game, он автоматически создает Board, а когда создается Board, он создает 81 (9x9) Tiles.

1. С иерархией представлений, как создаются соответствующие модели представлений?

Чтобы соответствовать иерархии моделей, я хотел бы иметь иерархию представлений (GameView содержит BoardView, который содержит 81 TileViews). В XAML довольно легко создать эту иерархию представлений с помощью пользовательских элементов управления, но я не понимаю, как создаются модели представлений.

В примерах, которые я видел, контекст данных пользовательского элемента управления часто устанавливается в модель представления (используя ViewModelLocator в качестве источника), который создает новый экземпляр модели представления. Кажется, это хорошо работает, если у вас есть плоский вид, но также кажется, что он становится беспорядочным, когда у вас есть иерархия. Создает ли GameView GameViewModel и оставляет его до своего дочернего элемента BoardView для создания BoardViewModel? Если да, то как GameViewModel взаимодействует с BoardViewModel? Может ли BoardViewModel связать резервную копию иерархии с GameViewModel?

2. Как модель представления получает данные модели?

В iOS я бы начал с использования службы для извлечения модели Game, которая была предварительно заполнена данными. Затем я создавал контроллер представления GameViewController (который отвечал за создание представления) и передавал ему Game. В MVVM я вижу, что значение имеет представление о создании собственной модели представления (в идеале с использованием ViewModelLocator), но я не понимаю, как эта модель представления получает модель.

Во всех примерах, которые я нашел в Интернете, модель представления использует некоторую услугу для извлечения собственных данных. Но я не сталкивался ни с одним примером, который принимает параметры конструктора или параметры, передаваемые с более высокого уровня навигации. Как это делается?

Я не хочу использовать ресурс приложения или какой-то другой метод хранения одноэлементной модели для своей модели, потому что, не то, что я делаю, но что, если бы я хотел отображать сразу несколько головоломок на экране? Каждый GameView должен содержать свой собственный Game.

Не только GameViewModel нужна ссылка на модель Game, но BoardViewModel, которая была каким-то образом создана (см. вопрос 1), нуждается в ссылке на модель Board, которая принадлежит Game модель. То же самое относится ко всем Tiles. Как вся эта информация передается по цепочке? Могу ли я сделать этот очень тяжелый подъем полностью в XAML, или мне придется делать какую-то привязку или другую инициализацию в коде?

Уф!

Я ценю любые советы, которые вы можете дать, даже если это не полный ответ. Я также хочу найти примеры проектов MVVM, которые имеют сходные проблемы для моих собственных. Спасибо тонну!

Ответ 1

Я бы начал с создания класса для запуска приложения. Обычно я называю этот класс чем-то вроде ApplicationViewModel или ShellViewModel, хотя технически он может придерживаться разных правил, чем то, что я обычно использовал бы для ViewModel

Этот класс получает экземпляр при запуске и является DataContext для ShellView или ApplicationView

// App.xaml.cs
private void OnStartup(object sender, StartupEventArgs e)
{
    var shellVM = new ShellViewModel(); 
    var shellView = new ShellView();    
    shellView.DataContext = shellVM;  
    shellView.Show(); 
}

Обычно это единственное место, где я устанавливаю DataContext для компонента UI напрямую. С этого момента ваши ViewModels являются приложением. Важно помнить об этом при работе с MVVM. Ваши представления - это просто удобный интерфейс, который позволяет пользователям взаимодействовать с ViewModels. На самом деле они не считаются частью кода приложения.

Например, ваш ShellViewModel может содержать:

  • BoardViewModel CurrentBoard
  • UserViewModel CurrentUser
  • ICommand NewGameCommand
  • ICommand ExitCommand

и ваш ShellView может содержать что-то вроде этого:

<DockPanel>
    <Button Command="{Binding NewGameCommand}" 
            Content="New Game" DockPanel.Dock="Top" />
    <ContentControl Content="{Binding CurrentBoard}" />
</DockPanel>

Это фактически превратит ваш объект BoardViewModel в пользовательский интерфейс как ContentControl.Content. Чтобы указать способ рисования BoardViewModel, вы можете указать DataTemplate в ContentControl.ContentTemplate или использовать неявный DataTemplates.

Неявный DataTemplate - это просто DataTemplate для класса, не связанного с ним x:Key. WPF будет использовать этот шаблон в любое время, когда он встречает объект указанного класса в пользовательском интерфейсе.

Таким образом, используя

<Window.Resources>
    <DataTemplate DataType="{x:Type local:BoardViewModel}">
        <local:BoardView />
    </DataTemplate>
</Window.Resources>

будет означать, что вместо рисования

<ContentControl>
    BoardViewModel
</ContentControl>

он рисует

<ContentControl>
    <local:BoardView />
</ContentControl>

Теперь BoardView может содержать что-то вроде

<ItemsControl ItemsSource="{Binding Squares}">
    <ItemsControl.ItemTemplate>
        <ItemsPanelTemplate>
            <UniformGrid Rows="3" Columns="3" />
        </ItemsPanelTemplate>
    <ItemsControl.ItemTemplate>
</ItemsControl>

и он нарисовал бы доску с использованием 3x3 UniformGrid, причем каждая ячейка содержала содержимое вашего массива Squares. Если ваше свойство BoardViewModel.Squares оказалось массивом объектов TileModel, то каждая ячейка ячейки будет содержать TileModel, и вы можете снова использовать неявный DataTemplate, чтобы сообщить WPF, как рисовать каждый TileModel

Теперь, как ваш ViewModel получает свои фактические объекты данных, это зависит от вас. Я предпочитаю абстрагировать весь доступ к данным за классом, например, Repository, а мой ViewModel просто вызывает что-то вроде SodokuRepository.GetSavedGame(gameId);. Это упрощает тестирование и поддержку приложения.

Однако вы получаете свои данные, имейте в виду, что ViewModel и Models являются вашим приложением, поэтому они должны нести ответственность за получение данных. Не делайте этого в View. Лично мне нравится поддерживать мой слой Model для простых объектов, содержащих только данные, поэтому всегда выполняйте операции доступа к данным из моих ViewModels.

Для связи между ViewModels у меня есть статья в моем блоге об этом. Подводя итог, используйте систему обмена сообщениями, такую ​​как Microsoft Prism EventAggregator или MVVM Light Messenger. Они работают как своего рода система подкачки: любой класс может подписаться на получение сообщений определенного типа, и любой класс может передавать сообщения.

Например, ваш ShellViewModel может подписаться на получение сообщений ExitProgram и закрыть приложение, когда он его услышит, и вы можете транслировать сообщение ExitProgram из любого места приложения.

Я предполагаю, что другой метод заключается в том, чтобы просто привязывать обработчики из одного класса к другому, такие как вызов CurrentBoardViewModel.ExitCommand += Exit; из ShellViewModel, но я нахожу это беспорядочным и предпочитаю использовать систему обмена сообщениями.

В любом случае, я надеюсь, что ответы на некоторые из ваших вопросов и укажут вам в правильном направлении. Goodluck с вашим проектом:)