在不同的ViewModel之间共享数据

21

我正在开发一个简单的MVVM项目,它有两个窗口:

  1. 第一个窗口是一个文本编辑器,在这个窗口中,我绑定了一些属性,如FontSizeBackgroundColor:

    <TextBlock FontSize="{Binding EditorFontSize}"></TextBlock>

它的DataContextMainWindowViewModel

public class MainWindowViewModel : BindableBase
{     
    public int EditorFontSize
    {
        get { return _editorFontSize; }
        set { SetProperty(ref _editorFontSize, value); }
    } 
.....
  1. 第二个窗口是选项窗口,在那里我有一个滑动条来改变字体大小:

<Slider Maximum="30" Minimum="10" Value="{Binding EditorFontSize }" ></Slider>

它的DataContextOptionViewModel

public class OptionViewModel: BindableBase
{     
    public int EditorFontSize
    {
        get { return _editorFontSize; }
        set { SetProperty(ref _editorFontSize, value); }
    }
.....

我的问题是我需要获取选项窗口中滑块的值,然后使用这个值修改的FontSize属性。但是我不知道如何将字体大小从OptionViewModel发送到MainViewModel。

我认为可以使用以下方法:

  1. 一个共享模型
  2. 在MainWindowViewModel中创建一个模型,并在OptionViewModel中引用此模型的引用
  3. 其他系统,如通知、消息等

希望你能帮我。这是我的第一个MVVM项目,英语不是我的主要语言:S

谢谢


1
我发誓——MVVM 中一半的问题都是关于这种情况的——两个窗口,每个窗口的视图模型如何通信。这很琐碎,我不确定为什么人们会有问题。但是,也许是因为我太蠢了。这个问题很好,很清晰,答案也很好。我将把它用作规范,并将这些问题标记为重复问题。 - user1228
6
可能是因为编写MVVM应用程序涉及到的概念不仅仅是MVVM本身,还包括依赖注入、多层设计以及服务和存储库的概念。初学者只看到MVVM作为XAML(视图)、模型(数据结构)和ViewModel(其他所有内容),没有意识到ViewModel和Model只是层而不是对象。模型不仅有数据结构,还有存储库(至少有接口)、服务和业务逻辑。当有人意识到这一点后,所有其他解决方案(例如事件聚合器只是另一个服务等)就变得很明显了。 - Tseng
3
@user1228建议这很简单,但被接受的答案是使用框架。如果必须使用框架,那么它就不是微不足道的了。 - Paul McCarthy
6个回答

16

另一个选项是将这样的“共享”变量存储在某种 SessionContext 类中:

public interface ISessionContext: INotifyPropertyChanged 
{
    int EditorFontSize { get;set; }
}

然后,将此内容注入您的视图模型中(您正在使用依赖项注入,对吧?)并注册到PropertyChanged事件:

public class MainWindowViewModel 
{
    public MainWindowViewModel(ISessionContext sessionContext)
    {
        sessionContext.PropertyChanged += OnSessionContextPropertyChanged;        
    }

    private void OnSessionContextPropertyChanged(object sender, PropertyChangedEventArgs e) 
    {
        if (e.PropertyName == "EditorFontSize")
        {
            this.EditorFontSize = sessionContext.EditorFontSize;
        }
    }       
}

1
简单而有效。我喜欢它。 - nikotromus

11

有许多方法可以在视图模型之间进行通信,但最好的方式是什么,这是一个很大的问题。以下是几种实现方式:

在我看来,最佳的方法是使用Prism框架的EventAggregator模式。该框架简化了MVVM模式。然而,如果您没有使用Prism,则可以使用Rachel Lim的教程——Rachel Lim简化版的EventAggregator模式。我强烈推荐您使用Rachel Lim的方法。

如果您使用Rachel Lim的教程,则应创建一个公共类:

public static class EventSystem
{...Here Publish and Subscribe methods to event...}

将一个事件发布到您的OptionViewModel中:

eventAggregator.GetEvent<ChangeStockEvent>().Publish(
new TickerSymbolSelectedMessage{ StockSymbol = “STOCK0” });

然后在另一个MainViewModel构造函数中订阅事件:

eventAggregator.GetEvent<ChangeStockEvent>().Subscribe(ShowNews);

public void ShowNews(TickerSymbolSelectedMessage msg)
{
   // Handle Event
}

Rachel Lim的简化方法是我见过最好的方法。然而,如果您想创建一个大型应用程序,则应阅读 Magnus Montin的文章,以及在CSharpcorner有一个示例

更新:对于版本大于5的PrismCompositePresentationEvent已被弃用,并在版本6中完全删除,因此您需要将其更改为PubSubEvent,其他所有内容均不变。


4
本来也想写一个类似的答案,但事件聚合器模式/消息总线是最好的方式。然而,我不同意使用静态事件系统,因为这是一种反模式,会使测试和切换组件变得更加困难。事件聚合器应该像所有其他服务和依赖项一样被注入到 ViewModel 中(最好使用 IoC 容器,但不是必须的)。 - Tseng
@Tseng,我非常同意你的观点,但是学习OP的棱镜确实很有必要。在我看来,如果我们只是开发简单的应用程序,那么使用Rachel Lim的方法会更容易些。 - StepUp
1
依赖注入不是Prism的一个特性,而且Prism本身也不是IoC容器。事件聚合器只是一个名称,它也可以是通过DI/IoC注入的自定义实现(只要它在容器的生命周期内是“静态”的,通常是应用程序的生命周期。只是没有static关键字 :))。 - Tseng
@Tseng 是的。我是Prism的追随者,因为Prism真正实现了关注点分离。我们有各种独立的“模块”来构建“应用程序的房子”。每个模块都可以由不同的团队创建,而不会干扰其他团队。我真的很喜欢Prism!:) - StepUp
1
Rachel Lim的代码略有过时,您需要在Prism版本> 6中将CompositePresentationEvent替换为PubSubEvent - user9401448
显示剩余2条评论

4

我已经用WPF完成了一个大型的MVVM应用程序。我有很多窗口,也遇到了同样的问题。我的解决方案可能不是非常优雅,但它完美地解决了问题。

第一个解决方案:我创建了一个唯一的ViewModel,并使用partial class将其拆分为多个文件。

所有这些文件都以以下内容开头:

namespace MyVMNameSpace
{
    public partial class MainWindowViewModel : DevExpress.Mvvm.ViewModelBase
    {
        ...
    }
}

我正在使用DevExpress,但看你的代码似乎应该尝试:

namespace MyVMNameSpace
{
    public partial class MainWindowViewModel : BindableBase
    {
        ...
    }
}
第二种解决方案:我还有几个不同的ViewModel来管理一些窗口。在这种情况下,如果我需要从一个ViewModel中读取一些变量到另一个ViewModel中,我将这些变量设置为静态(static)

例如:

    public static event EventHandler ListCOMChanged;
    private static List<string> p_ListCOM;
    public static List<string> ListCOM
    {
        get { return p_ListCOM; }
        set 
        {
            p_ListCOM = value;
            if (ListCOMChanged != null)
                ListCOMChanged(null, EventArgs.Empty);
        }
    }

也许第二种解决方案更简单,而且仍然适合您的需求。
我希望这很清楚。如果您需要更多细节,请问我。

2
我不是MVVM专家,但我遇到类似问题时的解决方案是,创建一个主类,将所有其他视图模型设置为其属性,并将此类设置为所有窗口的数据上下文。对于你的情况来说,这似乎已经足够了,但我不知道这是好还是坏。
如果需要更复杂的解决方案,请参考这个链接
如果需要更简单的解决方案,可以尝试以下操作:
public class MainViewModel : BindableBase
{
    FirstViewModel firstViewModel;

    public FirstViewModel FirstViewModel
    {
        get
        {
            return firstViewModel;
        }

        set
        {
            firstViewModel = value;
        }
    }

    public SecondViewModel SecondViewModel
    {
        get
        {
            return secondViewModel;
        }
        set
        {
            secondViewModel = value;
        }
    }

    SecondViewModel secondViewModel;

    public MainViewModel()
    {
        firstViewModel = new FirstViewModel();
        secondViewModel = new SecondViewModel();
    }
}

现在您需要为您的 OptionWindow 创建另一个构造函数,传递视图模型。
 public SecondWindow(BindableBase viewModel)
    {
        InitializeComponent();
        this.DataContext = viewModel;
    }

这是为了确保两个窗口在同一个视图模型实例上运行。

现在,只需在打开第二个窗口的任何位置使用以下两行代码:

var window = new SecondWindow((ViewModelBase)this.DataContext);
        window.Show();

现在你正在将第一个窗口的视图模型传递给第二个窗口,以便它们可以在同一个MainViewModel实例上工作。

一切都已经完成,你只需要将绑定设置为:

<TextBlock FontSize="{Binding FirstViewModel.EditorFontSize}"></TextBlock>
<TextBlock FontSize="{Binding SecondViewModel.EditorFontSize}"></TextBlock>

不用多说,第一个窗口的数据上下文是MainViewModel。

0

我对WPF还很陌生,但已经想出一个解决方案,并且好奇更有经验的人对此有什么看法。

我有一个“考试”选项卡和一个“模板”选项卡。在我的简单概念证明中,我希望每个选项卡“拥有”一个Exam对象,并能够访问另一个选项卡的Exam

我将每个选项卡的ViewModel定义为static,因为如果它是普通实例属性,我不知道一个选项卡如何获取另一个选项卡的实际实例。虽然它正在工作,但这感觉不对。

namespace Gui.Tabs.ExamsTab {
    public class GuiExam: INotifyPropertyChanged {
        private string _name = "Default exam name";
        public string Name { 
            get => _name;
            set {
                _name = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName="") {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public partial class ExamsHome : Page {
        public ExamsHome() {
            InitializeComponent();
            DataContext = ViewModel;
        }

        public static readonly ExamsTabViewModel ViewModel = new ExamsTabViewModel();
    }

    public class ExamsTabViewModel { 
        public GuiExam ExamsTabExam { get; set; } = new GuiExam() { Name = "Exam from Exams Tab" };
        public GuiExam FromTemplatesTab { get => TemplatesHome.ViewModel.TemplatesTabExam; }
    }
}

namespace Gui.Tabs.TemplatesTab {
    public partial class TemplatesHome : Page {
        public TemplatesHome() {
            InitializeComponent();
            DataContext = ViewModel;
        }

        public static readonly TemplatesTabViewModel ViewModel = new TemplatesTabViewModel();
    }

    public class TemplatesTabViewModel {
        public GuiExam TemplatesTabExam { get; set; } = new GuiExam() { Name = "Exam from Templates Tab" };
        public GuiExam FromExamTab { get => ExamsHome.ViewModel.ExamsTabExam; }
    }
}

然后所有内容都可以在 xaml 中访问:

TemplatesHome.xaml(摘录)

<StackPanel Grid.Row="0">
    <Label Content="From Exams Tab:"/>
    <Label FontWeight="Bold" Content="{Binding FromExamTab.Name}"/>
</StackPanel>

<StackPanel Grid.Row="1">
    <Label Content="Local Content:"/>
    <TextBox Text="{Binding TemplatesTabExam.Name, UpdateSourceTrigger=PropertyChanged}"
    HorizontalAlignment="Center" Width="200" FontSize="16"/>
</StackPanel>

ExamsHome.xaml (摘录)

<StackPanel Grid.Row="0">
    <Label Content="Local Content:"/>
    <TextBox Text="{Binding ExamsTabExam.Name, UpdateSourceTrigger=PropertyChanged}"
    HorizontalAlignment="Center" Width="200" FontSize="16"/>
</StackPanel>

<StackPanel Grid.Row="1">
    <Label Content="From Templates Tab:"/>
    <Label FontWeight="Bold" Content="{Binding FromTemplatesTab.Name}"/>
</StackPanel>

0
在MVVM中,模型是共享数据存储。我会将字体大小持久化在实现了INotifyPropertyChangedOptionsModel中。任何对字体大小感兴趣的视图模型都会订阅PropertyChanged
class OptionsModel : BindableBase
{
    public int FontSize {get; set;} // Assuming that BindableBase makes this setter invokes NotifyPropertyChanged
}

需要在字体大小更改时更新的ViewModels:
internal void Initialize(OptionsModel model)
{
    this.model = model;
    model.PropertyChanged += ModelPropertyChanged;

    // Initialize properties with data from the model
}

private void ModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(OptionsModel.FontSize))
    {
        // Update properties with data from the model
    }
}

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