MVVM和View/ViewModel层级结构

16

我正在使用C#和XAML为Windows 8制作我的第一个游戏。我仍在学习核心概念和最佳实践,MVVM一直是一个难点。我将尝试分两部分提出问题。

背景

我正在制作数独游戏。数独有一个包含9x9网格的板。我有三个模型-GameBoardTile。当创建一个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模型,而且某种方式(参见问题1)创建的BoardViewModel也需要引用属于Game模型的Board模型。对于所有的Tiles也是一样的。所有这些信息是如何向下传递的?我能够完全在XAML中完成这么多的繁重工作,还是需要在代码中进行一些绑定或其他初始化操作?
哇!非常感谢您能提供任何建议,即使不是完整的答案。我也很想找到与我自己面临的类似挑战相似的MVVM项目示例。非常感谢!

我认为你在谈论嵌套用户控件的问题(请参见http://catel.catenalogic.com/index.htm?gs_viewscontrols_nested_user_controls_problem.htm)。目前Catel本身尚不支持WinRT(虽然有一个beta版本),但至少你可以了解我认为应该如何完成。 - Geert van Horrik
1个回答

22
我会从创建一个类开始,用于启动应用程序。通常我会将该类命名为ApplicationViewModelShellViewModel,即使它可以遵循不同于我通常用于ViewModel的规则。

这个类在启动时被实例化,并成为ShellViewApplicationViewDataContext

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

这通常是我直接为UI组件设置DataContext的唯一位置。从此处开始,您的ViewModels就是应用程序。在使用MVVM时需要牢记这一点。您的Views只是一个用户友好的界面,允许用户与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对象呈现为UI的ContentControl.Content。要指定如何绘制BoardViewModel,您可以在ContentControl.ContentTemplate中指定DataTemplate,或使用隐式DataTemplates
隐式DataTemplate只是一个没有与之关联的x:Key的类的DataTemplate。每当WPF在UI中遇到指定类的对象时,它都会使用此模板。
因此,使用
<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消息。
我想另一种方法是仅将一个类的处理程序附加到另一个类,例如从ShellViewModel调用CurrentBoardViewModel.ExitCommand += Exit;,但我发现这很混乱,更喜欢使用消息系统。
无论如何,我希望这些回答能解决你的一些问题并指引你朝着正确的方向前进。祝你的项目好运 :)

哇,这是很多需要消化的内容!让我总结一下看看我是否理解正确:父视图将包含ContentTemplates,其中content属性设置为视图模型对象。DataTemplates定义这些ContentTemplates以包含相应的视图。这意味着您没有使用ViewModelLocator或IoC,对吗? - Brent Traut
此外,数据访问仍然让我担心。如果视图模型负责获取自己的模型,那么我是否可以指定有关使用哪个模型的其他信息?如果我有难题和简单的谜题,我该如何告诉我的新GameViewModel要使用哪个谜题? - Brent Traut
2
@BrentTraut 我个人不喜欢使用ViewModelLocator。 它有一些限制,比如您只能拥有一个ViewModel实例,并且不能传递参数。 我更喜欢在代码中构建我的应用程序,并使UI仅为我的类提供用户友好的界面。 - Rachel
1
@BrentTraut 关于数据访问的问题,我会将这种逻辑放在ViewModel中。无论是用户选择要加载的谜题(在这种情况下,您的ViewModel中将有一个ObservableCollection<GameBoard> AvailablePuzzlesGameBoard SelectedPuzzle),还是基于ViewModel中的其他逻辑来加载一个谜题。 - Rachel
@Rachel 你选择在XAML中通过指定ViewModels并使用DataTemplates将它们替换为相应的View对象来构建你的视觉层次结构(而不是直接指定Views并使用ViewModelLocator或其他绑定ViewModels的方法)。这种方法似乎使得在同一窗口中存在多个视图时更难微调视觉布局。例如,如果你在一个窗口中绑定了两个与同一类的ViewModels相关联的视图,则使用你的方法将很难改变一个视图的大小而不影响另一个视图。 - Roman
@Roman 我不太确定我理解你的意思。它背后的想法是让你的视图完全与你的视图模型分离。我一直发现使用这种方法非常容易操作和维护 UI 层,因为它完全独立于数据层。 - Rachel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接