【问题标题】:ICommand with ViewModel dependency具有 ViewModel 依赖项的 ICommand
【发布时间】:2025-11-23 04:25:02
【问题描述】:

当我使用 ICommand 时,我正在寻找一种模式以在我的应用程序中保留 SOLID 原则。基本上我的问题是命令执行与视图模型有依赖关系,但同时视图模型与命令有依赖关系(我通过构造函数注入它们)。我想只保留带有属性的视图模型,所以这是我当前实现的一个示例:

public class MyViewModel : INotifyPropertyChanged
{
   public ICommand MyCommand { get; private set; }

   public string Message { get; set; } // PropertyChanged ommited

   public MyViewModel()
   {            
   }

   public void SetCommand(ICommand myCommand)
   {
       this.MyCommand = myCommand;
   }

   ....
}

internal interface IMyViewModelCommandManager
{
    void ExectueMyCommand();
}

internal class MyViewModelCommandManager : IMyViewModelCommandManager
{
   private readOnly MyViewModel myViewModel;

   public MyViewModelCommandManager(MyViewModel myViewModel)
   {
       this.myViewModel = myViewModel;
   }

   public ExectueMyCommand()
   {
        MessageBox.Show(this.myViewModel.Message);
   }
}

internal class MyViewModelFactory: IMyViewModelFactory
{
   private readonly IContainerWrapper container;

   public MyViewModelFactory(IContainerWrapper container)
   {
      this.container = container;
   }

   public MyViewModel Create()
   {
       MyViewModel viewModel = new MyViewModel();

       IMyViewmodelCommandManager manager = this.container.Resolve<IMyViewmodelCommandManager>(new ResolverOverride[] { new ParameterOverride("viewModel", viewModel) });

       ICommand myCommand = new DelegateCommand(manager.ExecuteMyCommand);

       viewModel.SetCommand(myCommand);

       return viewModel;
   }
}

所以,要避免使用 SetCommand 方法。我想到了两种解决方案,但我不知道它们是否优雅。

第一种是将viewmodel依赖从构造函数移到更新代码的方法中,这样:

public class MyViewModel : INotifyPropertyChanged
{
   public ICommand MyCommand { get; private set; }

   public string Message { get; set; } // PropertyChanged ommited

   public MyViewModel(ICommand myCommand)
   {
       this.MyCommand = myCommand;            
   }

   ....
}

internal interface IMyViewModelCommandManager
{
    void ExectueMyCommand(MyViewModel viewModel);
}

internal class MyViewModelCommandManager : IMyViewModelCommandManager
{
   public MyViewModelCommandManager()
   {
       ....
   }

   public ExectueMyCommand(MyViewModel viewModel)
   {
        MessageBox.Show(myViewModel.Message);
   }
}

internal class MyViewModelFactory: IMyViewModelFactory
{
   private readonly IContainerWrapper container;

   public MyViewModelFactory(IContainerWrapper container)
   {
      this.container = container;
   }

   public MyViewModel Create()
   {
       IMyViewmodelCommandManager manager = this.container.Resolve<IMyViewmodelCommandManager>(..);

       ICommand myCommand = new DelegateCommand<MyViewModel>(manager.ExecuteMyCommand);

       MyViewModel viewModel = new MyViewModel(myCommand);
       return viewModel;
   }
}

当然,xaml 代码会使用 CommandParameter:

<Button Content="Show Message" Command="{Binding MyCommand}" CommandParameter="{Binding .}" />

我想到的其他解决方案是使用一种技巧来创建 viewModel 的 Wrapper,并且 commandManager 依赖于 Wrapper 而不是 viewModel:

internal class MyViewModelCommandContext
   {
      public MyViewModel ViewModel { get; set; }
   }

   public class MyViewModel : INotifyPropertyChanged
    {
       public ICommand MyCommand { get; private set; }

       public string Message { get; set; } // PropertyChanged ommited

       public MyViewModel(ICommand myCommand)
       {
           this.MyCommand = myCommand;            
       }

       ....
    }

    internal interface IMyViewModelCommandManager
    {
        void ExectueMyCommand();
    }

    internal class MyViewModelCommandManager : IMyViewModelCommandManager
    {
       private readonly MyViewModelCommandContext context;

       public MyViewModelCommandManager(MyViewModelCommandContext context)
       {
           this.context = context;
           ....
       }

       public ExectueMyCommand()
       {
            MessageBox.Show(this.context.myViewModel.Message);
       }
    }

    internal class MyViewModelFactory: IMyViewModelFactory
    {
       private readonly IContainerWrapper container;

       public MyViewModelFactory(IContainerWrapper container)
       {
          this.container = container;
       }

       public MyViewModel Create()
       {
           MyViewModelCommandContext context = new MyViewModelCommandContext();

           IMyViewmodelCommandManager manager = this.container.Resolve<IMyViewmodelCommandManager>(new ResolverOverride[] { new ParameterOverride("context", context) });

           ICommand myCommand = new DelegateCommand(manager.ExecuteMyCommand);

           MyViewModel viewModel = new MyViewModel(myCommand);
           context.ViewModel = viewModel;
           return viewModel;
       }
    }

在我看来,第一个是这个问题的最佳解决方案,你认为最好的解决方案是什么。你会应用其他解决方案吗?

【问题讨论】:

  • 将 command 属性留给private set 然后添加一个允许任何人设置属性的方法真的没有意义。您不妨将命令属性的set 保留为public,这没有什么问题。话虽如此,您的ICommand 非常通用,有人很容易在其位置注入一个完全出乎意料的命令。您最好使用RelayCommand(请参阅here)并将您的命令逻辑包含在视图模型中。
  • 我有私有集,因为我的意图是通过构造函数注入命令,因为我理解命令是视图模型的依赖,视图模型不能知道命令逻辑。如果我将设置更改为公共,任何人也可以注入意外命令。在我看来不是视图模型的责任来保持逻辑。

标签: c# wpf mvvm icommand


【解决方案1】:

恕我直言,这两种解决方案都过于复杂。 SOLID 很棒,KISS 更好。

您的MyViewModelCommandManager 目前直接与MyViewModel 耦合,因为它需要后者的Message,那么将它们分开有什么好处?为什么不简单地在MyViewModel 中实现命令呢?

如果这需要向MyViewModel 注入太多依赖项,那么请考虑一下您实际需要该命令做什么,并抽象出所有不需要的其他内容。

  • 该命令显示一条消息。
  • 邮件由MyViewModel持有
  • 您想在MyViewModel之外显示消息(可能其他视图模型也需要显示消息并且您想重用代码?)
  • 因此,您真正需要的只是来自MyViewModel 的某种通知,表明它想要显示一条消息,或者发生了会导致显示一条消息的事情。

可能的解决方案:

  1. IMessageDisplayService 注入MyViewModelMyViewModel 用消息调用它。
  2. MyViewModel 注入回调,类似于上面。
  3. MyViewModel 引发一个事件,并将消息作为 EventArg。

上述解决方案的推断职责略有不同。

  1. 表示MyViewModel负责。它想显示一条消息。
  2. 不太明确。 MyViewModel 知道它需要调用回调,但并不真正知道或关心它的作用。
  3. 与 2 类似,但更加解耦。多个事物可以订阅或取消订阅该事件,但 MyViewModel 仍然非常无知。

所有这三个都意味着显示消息的东西不需要知道MyViewModel。你已经将它们解耦了。 MyViewModelFactory 负责任何接线。

【讨论】:

  • 关于在 ViewModel 中实现消息:a) 由于命令在不同的类中将是私有的而不是公共的,因此更难测试。 b)它可能会为命令提供更多数据,而该命令应该能够处理。 c) 随着时间的推移,视图模型往往是具有很多职责的上帝般的对象。不要误会我的意思,我知道这里有问题,但我不认为混合职责是正确的解决方案。
  • 是的,我同意显示消息框可能不应该存在于 VM 中。我有点假设消息框只是为了示例的快速实现。请注意 OP,如果您真的要显示消息框,那么最好使用建议的替代方法之一将该功能移到外面。正如 Ignacio 所说,它使测试更容易,因为可以模拟和检查实现。
  • 没错,这是一个例子。但是视图模型可以包含二十个属性,并且该命令可以执行外部调用并使用视图模型的多个属性。这个例子是一个大项目的抽象。
【解决方案2】:

感谢您的意见。

当你说我正在创建一个复杂的模式时,我理解你,但是在一个拥有大型开发团队的大项目中,如果没有明确的模式和职责分工,代码维护可能无法执行。

阅读您和您的第三个解决方案,我想到了一种可能的解决方案。它看起来很复杂,但在我看来,它提高了代码质量。我将创建一个 commandContext,它只有代码所需的 viewmodel 属性,避免在命令管理器中拥有所有 viewmodel。此外,我将创建一个类,其职责是在视图模型更改时维护更新的上下文。这是可能的代码:

internal class MyCommandContext
{
    public string Message { get; set; }
}

public class MyViewModel : INotifyPropertyChanged
{
    public ICommand MyCommand { get; private set; }

    public string Message { get; set; } // PropertyChanged ommited

    public string OtherProperty { get; set; }

    public ObservableCollection<MyChildViewModel> Childs { get; set; }

    public MyViewModel(ICommand myCommand)
    {
        this.MyCommand = myCommand;            
    }

       ....
}

internal interface IMyViewModelCommandManager
{
    void ExectueMyCommand();
}

internal class MyViewModelCommandManager : IMyViewModelCommandManager
{
   private readonly MyCommandContext context;

   public MyViewModelCommandManager(MyViewModelCommandContext context)
   {
       this.context = context;
       ....
   }

   public ExectueMyCommand()
   {
        MessageBox.Show(this.context.Message);
   }
}

internal interface IMyViewModelCommandSynchronizer
{
    void Initialize();
}

internal class MyViewModelCommandSynchronizer : IMyViewModelCommandSynchronizer, IDisposable
{
     private readOnly MyViewModel viewModel;
     private readOnly MyCommandContext context;

     MyViewModelCommandSynchronizer(MyViewModel viewModel, MyCommandContext context)
     {
         this.viewModel = viewModel;
         this.context = context;
     }

     public void Initialize()
     {
         this.viewModel.PropertyChanged += this.ViewModelOnPropertyChanged;
     }

    private void ViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Message")
        {
             this.context.Message = this.viewModel.Message;
        }
    }

    // Dispose code to deattach the events.
}

internal class MyViewModelFactory: IMyViewModelFactory
{
   private readonly IContainerWrapper container;

   public MyViewModelFactory(IContainerWrapper container)
   {
       this.container = container;
   }

   public MyViewModel Create()
   {
       MyCommandContext context = new MyCommandContext();

       IMyViewmodelCommandManager manager = this.container.Resolve<IMyViewmodelCommandManager>(new ResolverOverride[] { new ParameterOverride("context", context) });

       ICommand myCommand = new DelegateCommand(manager.ExecuteMyCommand);

       MyViewModel viewModel = new MyViewModel(myCommand);

       IMyViewModelCommandSynchronizer synchronizer = this.container.Resolve<IMyViewmodelCommandManager>(new ResolverOverride[] { new ParameterOverride("context", context), new ParameterOverride("viewModel", viewModel) });

       synchronizer.Initialize();

       return viewModel;
   }
}

【讨论】: