【问题标题】:Interact between Model and ViewModel of different UserControls without violating MVVM不同UserControl的Model和ViewModel交互,不违反MVVM
【发布时间】:2017-01-09 11:03:03
【问题描述】:

编辑:添加具体示例来阐明我想要实现的目标。

申请方案如下:

为了使代码更简单,我将使用普通的 Messenger 类来代替 Prism 的事件聚合器。元组包含 Id 和字符串负载。

public static class Messenger
{
    public static event EventHandler<Tuple<int, string>> DoWork;

    public static void RaiseDoWork(int id, string path)
    {
        DoWork?.Invoke(null, new Tuple<int, string>(id, path));
    }
}

模型实例订阅 messenger 以了解何时开始工作(如果 Id 正确),并在工作完成时通知视图模型。

public class Model
{
    public int id;

    public Model(int id)
    {
        this.id = id;

        Messenger.DoWork += (sender, tuple) =>
        {
            if (tuple.Item1 != this.Id)
            {
                return;
            }

            var result = tuple.Item2 + " processed with id " + this.id;
            this.OnWorkCompleted(result);
        };
    }

    public event EventHandler<string> WorkCompleted;

    private void OnWorkCompleted(string path)
    {
        this.WorkCompleted?.Invoke(null, path);
    }
}

UserControlResult负责payload处理和结果输出。为了使代码更简单,我们只跟踪输出而不是将其放在 UI 上。所以 XAML 将是默认的。

代码隐藏:

public partial class UserControlResult : UserControl
{    
    private ResultViewModel viewModel;

    public UserControlResult()
    {
        this.InitializeComponent();
    }

    public void Init(int id)
    {
        this.viewModel = new ResultViewModel(id);
        this.DataContext = this.viewModel;
    }
}

视图模型:

public class ResultViewModel
{
    private Model model;

    public ResultViewModel(int id)
    {
        this.model = new Model(id);

        this.model.WorkCompleted += path =>
        {
            Trace.WriteLine(path);
        };
    }
}

UserControlButtons 包含按钮,其中一个应该通过信使开始处理UserControlResult 中的模型。为了使代码更简单,让我们省略命令实现,只显示它的处理程序。

代码隐藏:

public partial class UserControlButtons : UserControl
{    
    private ButtonsViewModel viewModel;

    public UserControlButtons()
    {
        this.InitializeComponent();
    }

    public void Init(int id)
    {
        this.viewModel = new ButtonsViewModel(id);
        this.DataContext = this.viewModel;
    }
}

视图模型:

public class ButtonsViewModel
{
    private int id;

    public ButtonsViewModel(int id)
    {
        this.id = id;
    }

    // DelegateCommand implementation...

    private void StartWorkingCommandHandler()
    {
        Messenger.RaiseDoWork(this.id, "test path");
    }
}

UserControlParent 包含UserControlResultUserControlButtons。他唯一的职责是将 ID 传递给他们,因此他甚至不需要视图模型。

Xaml:

<StackPanel>
    <uc:UserControlResult x:Name="UserControlResult" />
    <uc:UserControlButtons x:Name="UserControlButtons" />
</StackPanel>

代码隐藏:

public partial class UserControlParent : UserControl
{    
    public UserControlParent()
    {
        this.InitializeComponent();
    }

    public void Init(int id)
    {
        this.UserControlResult.Init(id);
        this.UserControlButtons.Init(id);
    }
}

最后,MainWindow 包含两个UserControlParent 实例。它的作用是为它们分配不同的 Id。

Xaml:

<StackPanel>
    <uc:UserControlParent x:Name="UserControlParent1" />
    <uc:UserControlParent x:Name="UserControlParent2" />
</StackPanel>

代码隐藏:

public partial class MainWindow : Window
{    
    public MainWindow()
    {
        this.InitializeComponent();

        this.UserControlParent1.Init(111);
        this.UserControlParent2.Init(222);
    }
}

这将起作用:按下UserControlButtons 中的按钮将开始在UserControlResult 模型中工作,并且UserControlParent 都将正确且独立地工作,这要归功于Id。

但我相信这个调用 Init 方法的链违反了 MVVM,因为代码隐藏(在 MVVM 中是 View)应该 strong> 了解有关 Id 值的任何信息(与 MVVM 中的 Model 相关)。说到这里,我确信 Id 不是视图模型的一部分,因为它在 UI 中没有任何表示。

如何在不违反 MVVM 的情况下将 Id 值从顶部窗口传递到“最深”视图模型?

原始问题

这是由 3 个 UserControls 组成的 WPF 应用程序:

UserControl3UserControl2 内容的一部分。我在开发和使用Prism的过程中保留MVVM

我需要从UserControl1view-model 调用UserControl3 中自定义类的方法(就MVVM 而言是model)。自定义类不能是单例的限制。我想通过以下方式之一做到这一点:

  1. 使用 Prism 的事件聚合器。 UserControl1 view-model 是发布者,UserControl3 model 是订阅者。为此,我需要在 Window 中创建唯一 ID,并将其传递给 UserControl1UserControl3

  2. Window 中创建服务实例并将其传递给UserControl1UserControl3。那么UserControl1 只会调用这个实例的方法。

  3. WindowUserControl2 实例传递给UserControl1UserControl1中的View-model只会调用UserControl2的方法,然后会调用UserControl3的方法,以此类推。

似乎 2 和 3 方法违反了 MVVM。您将如何解决这种情况?

【问题讨论】:

  • 发送命令或引发事件感觉像是没有将类紧密耦合在一起的更好方法......这两个概念之间的主要区别是:命令相当“实时”或目前任务同时事件是发生在过去的事情。
  • 如何有一个服务方法“in”一个控件?我建议您将服务方法放在其他地方,并让父视图模型将其传递给任何需要它们的视图模型(或者如果您不进行自动化测试,则将其设为全局单例)。任何对这个问题的解决方案即使承认存在控制也是错误的。控件不应该知道 Web 服务的存在;他们应该只知道他们的 DataContexts 的属性。
  • @EdPlunkett 我应该澄清一下:服务是指在UserControl3 的视图模型中存储为变量的普通类实例,因此它是@ MVVM 的 model 一部分987654368@。例如。该类接收图像的路径,异步加载和处理图像,然后将图像返回给UserControl3的view-model。问题是从UserControl1的视图模型正确启动这个过程。
  • @kayess 我建议您在谈论事件时指的是 1 个选项...但是如何使用命令在 不同 UserControls 的视图模型之间进行消息传递?当然,如果我理解正确的话。
  • @Sam 你一直在谈论控件。不要谈论控制,不要考虑控制。我认为您将您的程序视为其中包含视图模型的控件的集合,然后您尝试弄清楚视图模型如何进行通信。那不是 MVVM。在 MVVM 中,您的程序是视图模型的集合,然后您拥有显示它们的视图。设计视图模型关系。我再说一遍:“任何解决这个问题的方法,即使承认存在控件也是错误的。”

标签: c# wpf mvvm


【解决方案1】:

我会使用选项 1。我使用 MVVM Light 发送消息,接收到该特定消息的人将触发服务方法。松散耦合。

【讨论】:

  • 谢谢。但是如何将 Id 传递给 UserControl1UserControl3?正如我在问题中注意到的(关于单例的注释),当UserControl3 中的模型对象接收到消息(无论是 Prism 还是 MVVMLight 消息传递)时,它必须确保消息来自特定 UserControl1 的视图模型。因为我可以在单个Window 中拥有两组UserControls(如图所示)。
  • @Sam 你可以从ViewModelViewModelPubSubEvent 传递一个有效载荷,并在UserControl3 订阅它
  • @FilippoVigani 谢谢,我会查看PubSubEvent 并让您知道结果。
【解决方案2】:

我认为我实现了真正的 MVVM 实现,如下面的简化示例所示。特别感谢 Ed Plunkett 的 comment 和 Nikita 的 answer

首先,我不再需要传递唯一 ID。为了识别不同的ParentViewModel 实例,我只传递不同的Messenger 实例(为了简单起见,它替换了Prism 的EventAggregator):

internal class Messenger
{
    public event EventHandler<string> DoWork;

    public void RaiseDoWork(string path)
    {
        this.DoWork?.Invoke(this, path);
    }
}

其次,在我的特殊情况下,模型似乎不应该担心MessengerDoWork 事件。一旦在一个视图模型 (ButtonsViewModel) 中引发此事件,则该事件更适合由另一个视图模型 (ResultViewModel) 使用,而不是由 Model 本身使用。所以Model也简化了:

internal class Model
{
    public string Process(string input)
    {
        return input + " processed!";
    }
}

下面展示了“从上到下”的所有视图模型。

internal class MainViewModel
{
    private readonly Messenger eventAggregator1 = new Messenger();
    private readonly Messenger eventAggregator2 = new Messenger();

    public MainViewModel()
    {
        this.ParentViewModel1 = new ParentViewModel(this.eventAggregator1);
        this.ParentViewModel2 = new ParentViewModel(this.eventAggregator2);
    }

    public ParentViewModel ParentViewModel1 { get; }
    public ParentViewModel ParentViewModel2 { get; }
}

internal class ParentViewModel
{
    public ParentViewModel(Messenger eventAggregator)
    {
        this.ButtonsViewModel = new ButtonsViewModel(eventAggregator);
        this.ResultViewModel = new ResultViewModel(eventAggregator);
    }

    public ButtonsViewModel ButtonsViewModel { get; } 
    public ResultViewModel ResultViewModel { get; }
}

internal class ButtonsViewModel
{
    private readonly Messenger eventAggregator;

    public ButtonsViewModel(Messenger eventAggregator)
    {
        this.eventAggregator = eventAggregator;

        this.StartCommand = new DelegateCommand(this.StartProcessing);
    }

    public DelegateCommand StartCommand { get; }

    private void StartProcessing()
    {
        this.eventAggregator.RaiseDoWork("test path");
    }
}

internal class ResultViewModel : ViewModelBase
{
    private readonly Model model = new Model();
    private string textValue;

    public ResultViewModel(Messenger eventAggregator)
    {
        eventAggregator.DoWork += (sender, s) => this.DoWorkHandler(s);
    }

    public string TextValue
    {
        get { return this.textValue; }
        set { this.SetProperty(ref this.textValue, value); }
    }

    private void DoWorkHandler(string s)
    {
        var result = this.model.Process(s);
        this.TextValue = result;
    }
}

请注意,在ResultViewModel 中,我将Trace.WriteLine 替换为实际的屏幕输出(因为现在字符串没有ID,所以跟踪输出相同)。 ViewModelBase 只是实现了INotifyPropertyChanged

下面展示了“从上到下”所有视图的内容部分

<!-- MainWindow.xaml -->
<StackPanel Orientation="Horizontal">
    <views:UserControlParent DataContext="{Binding ParentViewModel1}" />
    <views:UserControlParent DataContext="{Binding ParentViewModel2}" />
</StackPanel>

<!-- UserControlParent.xaml -->
<StackPanel>
    <local:UserControlResult DataContext="{Binding ResultViewModel}" />
    <local:UserControlButtons DataContext="{Binding ButtonsViewModel}" />
</StackPanel>

<!-- UserControlButtons.xaml -->
<Grid>
    <Button Content="Test" Command="{Binding StartCommand}" />
</Grid>

<!-- UserControlResult.xaml -->
<Grid>
    <TextBlock Text="{Binding TextValue}" />
</Grid>

最后这两个世界在 App.xaml.cs 中连接起来:

private void App_OnStartup(object sender, StartupEventArgs e)
{
    new MainWindow { DataContext = new MainViewModel() }.Show();
}

看起来像 MVVM,但欢迎任何评论。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-10
    • 2012-03-19
    • 2015-12-15
    • 2014-07-31
    相关资源
    最近更新 更多