如何从主窗口中的 ViewModel 订阅事件?

How do I subscribe to an event from the ViewModel in the MainWindow?

我正在尝试了解如何从我的 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");
        }
    }
}

看来我需要做的就是在我的 NOTIFYING CLASS (ViewModel):

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

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

    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 不再听到它...为什么不呢?

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

为了回答您的问题,即如何处理视图模型,我建议将 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>