【问题标题】:MVVM: Provide a service to work with a specific part of the UI?MVVM:提供服务来处理 UI 的特定部分?
【发布时间】:2012-01-17 10:57:06
【问题描述】:

假设在我的应用程序中,有一个用于表示(地理)地图的用户界面。它以UserControl 的形式集成到应用程序中,并在其背后有其视图模型。

现在,假设我想为我的应用程序的其他部分提供通用服务接口,以在地图上执行常见任务(缩放、平移等),而不用担心 UI 细节。我可以放弃对视图模型的直接引用,但我很确定我会违反关注点分离原则,更不用说它的可测试性了。

所以有几个问题:

  1. 首先实现此类服务(充当 UI 的中间链接)是否有意义且是一种良好的做法?
  2. 既然服务直接在地图的视图模型上操作,那么实现服务接口的应该是视图模型本身吗?
  3. 服务接口提供事件是否合适(例如,除了提供改变地图比例的方法外,还提供改变地图比例的事件)?还是最好采用某种事件广播器(聚合器)机制将此类通知推送到服务接口之外?

提前感谢您的帮助。

【问题讨论】:

    标签: silverlight design-patterns mvvm


    【解决方案1】:

    当您需要两个视图模型进行通信时(或类似的东西,例如想要在视图模型以外的视图模型上调用命令的按钮),我发现的最佳实践是使用消息传递通道。 (MVVM Light 具有 Messenger 类;Prism 和 Caliburn.Micro 都有一个 EventAggregator。)当命令的目标被实例化(您的地图视图模型)时,它将在消息传递通道上注册特定命令。当一个调用程序(例如一个按钮)被实例化时,它就可以通过同一个通道发送命令。这使您的组件保持松散耦合。来自消息通道的命令可以很容易地模拟单元测试。它还为您开辟了其他途径,例如同时打开多个地图(只需使用不同的消息传递渠道或某种令牌)。

    在您的情况下,我会跳过整个服务接口。使用事件聚合器时,它并没有真正增加太多。根据代码库的大小和复杂性,您可能希望保留它,以便它描述可用于地图的命令,但这仅在您拥有多个命令时才有意义。在这种情况下,服务将注册为消息通道上命令的端点,然后必须将这些命令转发到地图视图模型。 (看到了吗?不会增加太多,只会让事情复杂化。)

    跳过事件。他们似乎没有添加任何东西。

    【讨论】:

    • 非常感谢。我对事件聚合器模式的问题是它隐藏了组件公开的接口(在最一般意义上)。此外,虽然我可以使用事件聚合器来发送通知,其他组件可能会或可能不会做出反应,但出于某种原因,我对另一种情况感到不舒服 - 当一个组件向其他组件发送消息时 是应该做出反应。
    • 换句话说,这种模式感觉适合类似事件的行为,而不是一个组件控制其他组件的操作的情况。
    • 在我看来,可能是命名将我推向了某种思维方式——而 Prism 中的 EventAggregator 让我按照我刚才描述的方式思考,而 Messenger 听起来更像是一种通用机制以及更多符合您建议的内容,而它们的作用基本相同。
    • 我相信不同的库作者会告诉您 EventAggregator 和 Messenger 之间存在差异。但是,没有理由不能将它们视为同一事物。在实践中,我将通过 Prism 的 EventAggregator 发送的所有“事件”命名为命令(例如:DoSomethingCommand)。
    【解决方案2】:

    EventAggregator 是另一种在断开的视图模型之间建立通信的机制。我相信您的应用程序的所有其他部分都将使用相同的 MVVm,并且将使用 viewmodel 来执行操作。使用来自应用程序其他部分的所需参数发布事件,例如 Zoom,并使用订阅机制在地图中捕获它。

    http://msdn.microsoft.com/en-us/magazine/dd943055.aspx#id0420209

    Prism 具有良好的事件聚合器实现。你可以使用那个部分。

    【讨论】:

      【解决方案3】:

      考虑使用 MVVM Light 工具包中的 Messenger。在另一个 SO 答案中查看更多信息:

      https://stackoverflow.com/a/2700324/117625

      【讨论】:

        【解决方案4】:

        如果您有一个指定适当行为的聚合 Command 对象怎么办?我将尝试将您的问题具体化一点,如果我错了,请纠正我:

        假设您的应用有两个相关部分 - 一个可以缩放和平移等的地图组件,以及一组控件,它们提供用于缩放、平移和在它们之间选择的用户界面 - 有点像一组模式选择器。您不希望它们中的任何一个直接引用另一个,并且诱惑是让地图直接了解其控件集,以便它可以从它们捕获事件并适当地切换模式状态。

        解决这个问题的一种方法是在一个对象中注入一组 CompositeCommands(可从Prism Application Guidance 获得)库。通过这种方式,您可以获得解耦和对接口的强烈描述(如果您愿意,也可以使用事件)。

        public class MapNavigationCommands{
          public static CompositeCommand startPanning = new CompositeCommand();
          public static CompositeCommand startZooming = new CompositeCommand();
          public static CompositeCommand setViewbox = new CompositeCommand();
        }
        

        您的模式控件,在功能区中,向您的 DI 框架注册以进行注入(不想在此示例中引入 DI,我只是直接引用了这些静态成员)。

        public class ModeControls : UserControl{
          ...
          public void PanButtonSelected(object sender, RoutedEventArgs e){
            MapNavigationCommands.StartPanning.Execute(this); //It doesn't really care who sent it, it's just good event practice to specify the event/command source.
          }
        }
        

        或者,在 XAML 中:

        ...
          <Button Command={x:Static yourXmlns:MapNavigationCommands.StartPanning}>Start</Button>
        ...
        

        现在,在地图一侧:

        public class PannableMapViewModel{
          public PannableMapViewModel(){
            MapNavigationCommands.StartPanning.RegisterCommand(new DelegateCommand<object>(StartPanning));
            MapNavigationCommands.SetViewbox.RegisterCommand(new DelegateCommand<Rectangle>(SetViewBox));
        
          }
          private void StartPanning(object sender){
            this.SetMode(Mode.Pan); //Or as appropriate to your application.  The View is bound to this mode state
          }
          private void SetViewbox(Rectangle newView){
            //Apply appropriate transforms.  The View is bound to your transform state.
          }
        }
        

        现在您在两个控件之间有了一个解耦的、强指定的接口,保持 ViewModel 分离,可以为您的测试模拟出来。

        【讨论】:

        • 有趣的方法。在研究了 Prism 命令和复合命令之后,我从来没有想过它们可以以这种方式使用。你知道一些使用这种方法的开源项目(或文章)吗?
        • 人力资源部。抱歉,我想我不会。当我们逐渐采用 MVVM 和 Prism 时,我们就是这样做的——只是在没有任何其他基础设施的情况下引入指挥(为了记录,当我们开始使用它时,其余的基础设施是完全值得的,下次我不会逐渐采用相同的方式时,我只会遇到批发。棱镜非常好)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-26
        • 2012-02-12
        • 2014-02-05
        • 2016-08-05
        • 1970-01-01
        相关资源
        最近更新 更多