将ViewModel绑定到多个窗口

9
我正在将一个做羊毛剪切比赛计分的Windows Forms项目(不要问,这在新西兰是一项非常受欢迎的运动)从VB.NET重写为WPF C#。我遇到了一个问题,似乎无法解决。
我有两个窗口。一个是源窗口,在其中输入内容(例如当前事件名称),另一个窗口会以闪现的方式显示此信息以投影到屏幕上(因此会在第二个监视器上),同时还会通过网络上的XML传输其他数据。我已经将其设置为MVVM,ViewModel和Model作为独立的项目。
在我的主窗口中,我可以很好地绑定控件,并且如果我在一个文本框中键入,则如果它与相同的内容绑定,则它会立即出现在另一个文本框中。 然而,在第二个窗口中,我将控件绑定到相同的位置,但它没有更新。
我已经困扰了一个星期,网络上的每个示例都展示如何在一个窗口上执行操作,我已经成功地完成了这项工作,但缺少两个窗口的示例。
以下是我的代码...
这是我的ViewModel项目中的内容:
namespace SheepViewModel
{
public class SheepViewModel : INotifyPropertyChanged


{
    private string _CurrentEventName;
    static SheepViewModel _details;

    public string CurrentEventName
    {
        get { return _CurrentEventName; }
        set
        {
            _CurrentEventName = value;
            OnPropertyChanged("CurrentEventName");
        }
    }

    public static SheepViewModel GetDetails()
    {
        if (_details == null)
            _details = new SheepViewModel();
        return _details;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string prop)
    {
        if (PropertyChanged != null)
             PropertyChanged(this, new PropertyChangedEventArgs(prop));
            Console.WriteLine("Test");
            }     
    }
}

然后我有一个主窗口,除了一行代码打开我们将要讲到的第二个窗口之外,实际上没有其他真正的代码...

 public MainWindow()
    {
        ScoreScreen SW = new ScoreScreen();
        SW.Show();
        InitializeComponent();
    }

然后是XAML。
<Window x:Class="Sheep_Score_3._1.MainWindow"
    xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="clr-namespace:SheepViewModel;assembly=SheepViewModel"
    mc:Ignorable="d"
    Title="MainWindow" Height="433.689" Width="941.194">
<Window.DataContext>
    <vm:SheepViewModel/>
</Window.DataContext>
<Window.Resources>
<Grid Margin="0,0,0,0">
<TextBox x:Name="CurrentEventName" Height="23" Margin="131.01,163.013,0,0" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Left" Width="327.151" Text="{Binding CurrentEventName, Mode=TwoWay}"/>
    <TextBox Text="{Binding CurrentEventName, Mode=TwoWay}" Margin="39.605,0,0,108.567" Height="49.111" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="399" />
</Grid>

以上代码都可以正常工作,如果我在第一个文本框中输入文本,它会出现在第二个文本框中。如果我在通知部分添加console.writeline,则可以看到它正在更新。

现在我添加了第二个窗口,设置完全相同...

<Window x:Class="Sheep_Score_3._1.ScoreScreen"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="clr-namespace:SheepViewModel;assembly=SheepViewModel"
    mc:Ignorable="d"
    Title="ScoreScreen" Height="300" Width="300">
<Window.DataContext>
    <vm:SheepViewModel/>
</Window.DataContext>
<Grid>
    <TextBox x:Name="textBlock" HorizontalAlignment="Left" Margin="79.374,116.672,0,0" TextWrapping="Wrap" Text="{Binding CurrentEventName, Mode=TwoWay}" VerticalAlignment="Top"/>
</Grid>

再次说明,这里没有实际的代码。

奇怪的是,如果我将此控件设置为双向并在其中输入文本,我可以看到它触发了相同的通知部分,但它没有更新另一个窗口。

我不确定我错过了什么,请帮助指引我正确的方向,谢谢。

4个回答

15

这是因为 两个窗口必须共享 ViewModel 的完全相同的实例

您所有的属性都是实例属性,比如

public string CurrentEventName { get { // snip

因此,所有值都对每个实例都是唯一的。您正在创建两个实例,每个窗口一个。

<Window x:Class="Sheep_Score_3._1.MainWindow"
    xmlns:blah="http://inurxamlskippinurschemas.org">
    <Window.DataContext>
        <vm:SheepViewModel/>
    </Window.DataContext>

那是一个实例,这是另一个实例

<Window x:Class="Sheep_Score_3._1.ScoreScreen"
        xmlns:blah="http://yaddayaddawhocares.derp">
    <Window.DataContext>
        <vm:SheepViewModel/>
    </Window.DataContext>

记住,xaml只是标记语言,可以反序列化为对象图。您有两个不同的标记文件,它们包含其中描述的所有不同实例。

这没有任何问题,使用实例属性的视图模型也没有问题。事实上,这比使用静态和静态绑定更受欢迎。

答案很简单。您需要将两个窗口都传递相同的视图模型实例

首先,从两个窗口中删除所有<Window.DataContext>的无用内容。那不是给你的。现在,只需更改构造函数为

public MainWindow()
{
    var viewModel = new SheepViewModel();
    ScoreScreen SW = new ScoreScreen();
    SW.DataContext = viewModel;
    SW.Show();
    InitializeComponent();
    //NOTICE!  After Init is called!
    DataContext = viewModel;
}

完成了。


非常感谢,非常感谢,非常感谢,这个解决了问题。 这很有道理,我有一种感觉它们是两个独立的实例,但来自vb背景,我不确定如何像这样将它们链接起来。 - Toby Mills
哈哈哈哈!因为“yaddayaddawhocares.derp”而加1。 - Chris W.
@TobyMills 如果你对实例不确定,你可以使用对象ID来跟踪它们。https://blogs.msdn.microsoft.com/zainnab/2010/03/04/make-object-id/ 祝你修剪顺利!哦,如果你在首映重写时提到我,那将是超级棒的。 - user1228
五周后这里将会直播“Golden Shears”比赛,请收看。https://www.google.com.au/search?q=Golden+shears+TV&ie=&oe= - Toby Mills
由于在XAML文件中指定DataContext会使Visual Studio设计器能够在IntelliSense中提供DataContext的成员,因此您可以说 SW.DataContext = DataContext,并将 <Window.DataContext> 中的无聊代码留在MainWindow中,在其他窗口的XAML中指定 d:DataContext 属性。或者只需在两个窗口中指定 d:DataContext,但构造函数应保持@Will编写的不变。 - Adam L. S.

1
我怀疑每个窗口都在创建自己的 ViewModel 实例。您可以尝试以下操作:
public MainWindow()
{
    InitializeComponent();

    SheepViewModel svm = new SheepViewModel();
    this.DataContext = svm;

    ScoreScreen SW = new ScoreScreen();
    SW.DataContext = svm;
    SW.Show();        
}

不确定在InitializeComponent中是否设置了来自xaml的DataContext,因此此时可能会出现null的情况。 - user1228
1
你可以在初始化之后设置数据上下文。 - TrueEddie
嗯,不太确定,所以我花了一分钟来验证一下... DataContext 在 InitializeComponent 之后填充,而不是之前,所以你原来的代码应该也可以工作。 - user1228

0
我会将ViewModel作为应用程序资源。
<Application.Resources>
    <VM:ViewModel x:Key="SharedViewModel" />
    ....
</Application.Resources>

然后在每个窗口中这样调用它

DataContext="{StaticResource MainViewModel}"

我对这个概念还很陌生,不确定这是否是最优解。但它确实能够工作!


0
您的ViewModel定义了一个静态方法来获取单个实例,但是您没有使用它来实例化。目前,您的ViewModel是使用默认构造函数创建的,这意味着这两个窗口将具有不同的副本。
在InitializeComponent下面或OnNavigatedToEvent中的代码后面创建您的ViewModel。
以下是进一步说明的一些代码:
在两个窗口中都像这样定义一个ViewModel属性。
property SheepViewModel ViewModel { get; set; }

然后是你的构造函数:

public MainWindow()
{
    InitializeComponent();
    ViewModel = SheepViewModel.GetDetails(); // include this line
    ScoreScreen SW = new ScoreScreen();
    SW.Show();
}

同时也删除

<vm:SheepViewModel/>

从 XAML 中移除,因为它不是必需的。


那不对。他的两个窗口已经有了一个专门为他的视图模型设计的属性——Window.DataContext。 - user1228
请再读一遍我的回答,我指出它调用了默认构造函数,因此它并没有按照原始提问者的意图工作。原始提问者编写了一个静态方法来获取实例,而我仅仅是向他展示如何使用它。 - Lou Watson
请再读一遍我的评论-你说他应该在两个窗口中“定义一个ViewModel属性”,这是非常不正确的。视图模型应该放在DataContext中,而不是某个随机属性中。 - user1228
ViewModel不是一个随意的属性,它是在Win 10发布以来与Binding和x:Bind一起使用的。在msdn上有一些很棒的文章,比如深入了解数据绑定,你可能会觉得很有趣。 - Lou Watson
啊,我明白了混淆的原因... OP的应用程序是普通的WPF,这意味着绑定与DataContext一起工作。你的例子可能在UWP中有效,但在这里无效——检查他的代码,他正在使用绑定。我可以很刻薄地建议你去翻阅古老的Windows Presentation Foundation文档,但为什么要麻烦呢?Bind才是未来! - user1228
它仍然可以在UWP中使用绑定,只是需要通过CollectionViewSource等轻微的迂回方式实现。 - Lou Watson

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