【问题标题】:How do I subscribe to an event from the ViewModel in the MainWindow?如何从 MainWindow 中的 ViewModel 订阅事件?
【发布时间】:2020-08-11 07:57:39
【问题描述】:

我试图弄清楚如何从 MainWindow.xaml.cs 订阅在 ViewModel 中触发的事件。

视图模型:

public class LoginViewModel : INotifyPropertyChanged
{
    private bool isAuthenticatedUser;
    public bool IsAuthenticatedUser 
    {
        get { return isAuthenticatedUser; }

        set
        {
            Debug.WriteLine("Old value:" + isAuthenticatedUser);

            isAuthenticatedUser = value;
            Debug.WriteLine("New value:" + isAuthenticatedUser);

            OnNotifyPropertyChanged("IsAuthenticatedUser");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnNotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            Debug.WriteLine($"Property Change on LoginView.IsAuthenticatedUser");
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

MainWindow.xaml.cs

public partial class MainWindow
{
    private int? oldUnreadTextsCount = 0;

    public MainWindow()
    {
        DataContext = new MainWindowViewModel();
        InitializeComponent();
        InitializeTimer();
        Loaded += MainWindow_Loaded;
    }

    public TextsViewModel TextsViewModel { get; set; }
    public LoginViewModel LoginViewModel { get; set; }
    public MainWindowViewModel MainWindowViewModel { get; set; }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        LoginViewModel = new LoginViewModel();

        // First Subscribe;
        LoginViewModel.PropertyChanged += UserAuthentication_PropertyChange;

        // Second Fire Change / Fire update on UserAuthentication_PropertyChange.
        LoginViewModel.TestAuthentication();

        // Third Change Views appropriately
        if (LoginViewModel.IsAuthenticatedUser)
        {
            NavigationFrame.NavigationService.Navigate(new HomeView());

            TextsViewModel = new TextsViewModel();
            TextsViewModel.PropertyChanged += UnreadTexts_PropertyChanged;
        }
        else
        {
            NavigationFrame.NavigationService.Navigate(new LoginView());
        }
    }

    private void UserAuthentication_PropertyChange(object sender, PropertyChangedEventArgs e)
    {
        Debug.WriteLine("PROPERTY CHANGE REGISTERED:MainWindow:Checking if logged in.");

        if (LoginViewModel.IsAuthenticatedUser)
        {
            Header.Visibility = Visibility.Visible;
            Footer.Visibility = Visibility.Visible;
            Debug.WriteLine("Logged In");
        }
        else
        {
            Header.Visibility = Visibility.Hidden;
            Footer.Visibility = Visibility.Hidden;
            Debug.WriteLine("Logged Out");
        }
    }
}

看来我需要做的就是在我的通知类(ViewModel)中:

  • 实现INotifyPropertyChanged

  • 添加public event PropertyChangedEventHandler PropertyChanged;

  • 添加OnNotifyPropertyChanged("IsAuthenticatedUser");

  • 将以下方法添加到我的通知类中:

protected void OnNotifyPropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
    {
        Debug.WriteLine($"Property Change on LoginView.IsAuthenticatedUser");
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

然后在我的 SUBSCRIBING 类(MainViewWindow)中添加以下内容:

  • 订阅我的 ViewModel 中的事件LoginViewModel.PropertyChanged += UserAuthentication_PropertyChange;
  • 添加订阅中引用的方法,如下所示
    private void UserAuthentication_PropertyChange(object sender, PropertyChangedEventArgs e)
    {
        Debug.WriteLine("PROPERTY CHANGE REGISTERED:MainWindow:Checking if logged in.");

        if (LoginViewModel.IsAuthenticatedUser)
        {
            Header.Visibility = Visibility.Visible;
            Footer.Visibility = Visibility.Visible;
            Debug.WriteLine("Logged In");
        }
        else
        {
            Header.Visibility = Visibility.Hidden;
            Footer.Visibility = Visibility.Hidden;
            Debug.WriteLine("Logged Out");
        }
    }

不幸的是,这似乎不起作用。订阅的方法 UserAuthentication_PropertyChange 仅在应用程序首次启动并且 ViewModel 上的 IsAuthenticatedUser 属性值发生变化时触发一次。为什么每次发生变化时都不起作用?

有关更多信息,如果您能帮助我(对我而言)理解这种现象,这里是应用程序运行时打印到控制台的内容的简要说明,我触发登录和注销序列。

启动应用程序 - 当前已注销。

TestAuthentication:LoginViewModel:False
Old value:False
New value:False
Property Change on LoginView.IsAuthenticatedUser
PROPERTY CHANGE REGISTERED:MainWindow:Checking if logged in.
Logged Out

耶!它看起来像它的工作......但是等等 - 还有更多......

然后我登录

AuthenticateUser:LoginViewModel:True
Old value:False
New value:True
Property Change on LoginView.IsAuthenticatedUser

属性更改已触发,但 MainWindow 不再听到它...为什么不呢?

【问题讨论】:

  • 您是如何实际登录的?我看到DataContextMainWindowViewModel。那么如何触发LoginViewModel的登录逻辑。现在,您似乎正在对 LoginViewModel 的两个实例进行操作。您只收听您在 Loaded 处理程序中创建的实例的事件,但是当您登录时,您使用的是另一个可能是 LoginViewModel 的 XAML 实例?
  • 是的,没错,当我使用NavigationFrame.NavigationService.Navigate(new LoginView()); 创建登录页面时,我在该页面的codeBehind 中实例化了一个新的LoginViewModel。是否有 2 个这样的实例导致问题?
  • 是的,这就是我要说的。您只订阅了一个实例的PropertyChanged。此实例与您用于通过登录页面进行真实登录的实例不同。另一个副作用是两个视图模型具有不同的状态。如果IsAuthenticatedUser 在一个实例中是true,那么在另一个实例中它仍然是false。你绝对不想要这个。确保您只使用每个视图模型的一个实例,除非您故意想要拥有多个实例 - 但在这种情况下,您需要一个共享/重用实例。
  • 嗯。这很有意义,而且我以前没有考虑过。状态显然会从一个实例变为另一个实例。你会如何推荐分享我的 ViewModel?在这种情况下我可以注入这个 ViewModel 并且可能没问题,但其他情况呢?有没有办法让 ViewModel 的每个实例都可以全局访问?例如,我可以将LoginViewModel 实例传递到我的导航方法中,因为这是我真正需要访问它的唯一地方——但这并不总是正确的。我需要让我的TextViewModel 可以在多个地方访问。
  • 您也可以将其作为答案,以便我接受。我已经测试过了,你解决了这个问题!!!非常感谢您帮助我理解这一点!!!

标签: c# wpf events inotifypropertychanged


【解决方案1】:

现在,您似乎正在对两个 LoginViewModel 实例进行操作。您只收听您在 Loaded 处理程序中创建的实例的事件,但是当您登录时,您可能正在使用不同的 LoginViewModel 的 XAML 实例。

要回答您的问题,如何处理视图模型,我建议将 Frame 替换为 ContentControl
这要求每个视图都有自己的视图模型,例如登录视图模型 --> 登录视图、主页视图模型 --> 主页视图。此外,每个视图都必须在 DataTemplate 中定义。
ContentControl 绑定到 SelectedPage 属性并通过应用适当的 DataTemplate 来呈现页面。

现在通过将MainWindowViewModelSelectedPage 属性设置为您希望导航到的页面的视图模型,可以在视图模型中进行导航。
通过这种方式,您可以将导航移动到视图模型并轻松处理事件等,因为视图模型在使用组合(或聚合)时了解其他视图模型。

PageName.cs
在 XAML 和 C# 中枚举以消除魔术字符串作为页面标识符。
使用时可以用作CommandParameter,例如使用Button.Command 导航到某个页面。

enum PageName
{
  LoginView, HomeView
}

MainWindowViewModel.cs

public class MainWindowViewModel : INotifyPropertyChanged
{  
  public TextViewModel TextViewModel { get; set; }
  private Dictionary<PageName, object> Pages  { get; set; }

  private object selectedPage;   
  public object SelectedPage
  {
    get => this.selectedPage;
    set 
    { 
      this.selectedPage = value; 
      OnPropertyChanged();
    }
  }

  private bool isAuthenticated;   
  public bool IsAuthenticated
  {
    get => this.isAuthenticated;
    set 
    { 
      this.isAuthenticated = value; 
      OnPropertyChanged();
    }
  }

  public MainWindowViewModel()
  {
    // Alternatively use constructor injection

    var loginPageViewModel = new LoginViewModel();
    loginPageViewModel.AuthenticationStatusChanged += OnAuthenticationStatusChanged;

    this.Pages = new Dictionary<PageName, object>()
    {
      { PageName.LoginView, loginPageViewModel },
      { PageName.HomeView, new HomeViewModel() }
    }

    // Show login screen initially
    this.SelectedPage = Pages[PageName.LoginView];

    this.TextViewModel = new TextViewModel();
  }

  // Example ICommand execute action, triggered e.g. on Button.Command,
  // where the CommandParameter is a value of the PageName enumeration
  private void ExecuteNavigateToPage(object commandParameter)
  {    
    if (commandParameter is PageName pageName
      && this.Pages.TryGetValue(pageName, out object pageViewModel)
    {
      this.SelectedPage = pageViewModel;
    }
  }

  private void OnAuthenticationStatusChanged(object sender, EventArgs e)
  {    
    this.IsAuthenticated = (sender as LoginViewModel).IsAuthenticatedUser;

    // Redirect to main page when user has authenticated successfully        
    if (this.IsAuthenticated)
    {
      this.SelectedPage = Pages[PageName.HomeView];
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

LoginViewModel.cs

public class LoginViewModel : INotifyPropertyChanged
{  
  public event EventHandler AuthenticationStatusChanged;    
  public event PropertyChangedEventHandler PropertyChanged;

  private bool isAuthenticatedUser;   
  public bool IsAuthenticatedUser
  {
    get => this.isAuthenticatedUser;
    set 
    { 
      this.isAuthenticatedUser = value; 
      OnPropertyChanged();

      OnAuthenticationStatusChanged();
    }
  }

  private void OnAuthenticationStatusChanged()
  {
    this.AuthenticationStatusChanged?.Invoke(this, EventArgs.Empty);
  }

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

** MainWindow.xaml.cs**

public partial class MainWindow
{
  private int? oldUnreadTextsCount = 0;

  public MainWindow()
  {
    InitializeComponent();
    InitializeTimer();

    var mainWindowViewModel = new MainWindowViewModel();
    DataContext = mainWindowViewModel;

    mainWindowViewModel.TextsViewModel.PropertyChanged += UnreadTexts_PropertyChanged;

    Loaded += MainWindow_Loaded;
  }

  private void MainWindow_Loaded(object sender, RoutedEventArgs e)
  {
    // Navigation (and debugger output) has been moved to the MainWindowViewModel.
    // Toggling of the visibility has been moved to XAML using DataTrigger
  }
}

LoginView.xaml

<UserControl x:Class="LoginView">

  <!-- 
    DataContext is automatically the LoginViewModel, 
    accessible from code-behind via the DataContext property 
  -->
  <TextBlock Text="{Binding IsAuthenticatedUser}" />
</UserControl>

HomeView.xaml

<UserControl x:Class="LoginView">

  <!-- 
    DataContext is automatically the HomeViewModel, 
    accessible from code-behind via the DataContext property 
  -->
  <TextBlock Text="Welcome back user!" />
</UserControl>

MainWindow.xaml

<Window>
  <Window.Resources>
    <DataTemplate DataType="{x:Type LoginViewModel}">
      <LoginView />
    </DataTemplate>

    <DataTemplate DataType="{x:Type HomeViewModel}">
      <HomeView />
    </DataTemplate>
  </Window.Resources>

  <StackPanel>
    <TextBlock x:Name="Header">
      <TextBlock.Style>
       <Style TargetType="TextBlock">
         <Setter Property="Visibility" Value="Hidden" />

         <Style.Triggers>
           <DataTrigger Binding="{Binding IsAuthenticated}" Value="True">
             <Setter Property="Visibility" Value="Visible" />
           </DataTrigger>
         </Style.Triggers>
       </Style>
      </TextBlock>
    </TextBlock>

    <!-- 
      Page host. Setting the content to a page view model,
      will automatically load the corresponding DataTemplate
    -->
    <ContentControl Content="{Binding SelectedPage}" />

    <TextBlock x:Name="Footer">
      <TextBlock.Style>
       <Style TargetType="TextBlock">
         <Setter Property="Visibility" Value="Hidden" />

         <Style.Triggers>
           <DataTrigger Binding="{Binding IsAuthenticated}" Value="True">
             <Setter Property="Visibility" Value="Visible" />
           </DataTrigger>
         </Style.Triggers>
       </Style>
      </TextBlock>
    </TextBlock>
</Window>

【讨论】:

    猜你喜欢
    • 2011-12-11
    • 2011-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多