在切换视图时禁用 CanExecute

问题描述

我有一个静态属性来跟踪我的视图模型中的属性变化:

public static class Global
{
   public static int Warning
   {
       get { return _warning; }
       set { _warning = value; OnStaticPropertyChanged(); }
   }
   private static int _warning;
}

然后,在视图模型中,我有 CanExecute 命令(用于取消更改的按钮):

private bool Cancel_CanExecute(object parameter)
{
     bool changes=false;

     if (!string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(SurName))
     { 
        changes= true;
     }

     if (changes)
     {
       Global.Warning = 1; //Changes are true,store this state
     }

     return changes;
}

当我的视图发生变化并且用户想要切换视图时,我想向他们显示一个警告消息框。所以在我的主窗口 viewmodel 我有一个改变视图的命令:

private void Open_view(object parameter)
{
   if (Global.Warning != 0)
   {
       var msg = _windowservice.ShowMessage("Want to leave? Changes won't be saved.");

       if (msg == true) //User clicked OK in MessageBox
       {
          Global.Warning = 0; // reset static property to default,and proceed with code
       }
       else
       {
           return; //Prevent opening new view
       }
               
    }
    //Switch to new view - Calling this line before 'Global.Warning = 0;' doesn't help either
    Open_view = new Someviewmodel();

    //...
}

但是,当我确认 MessageBox 离开视图而不保存更改并且将静态属性 Warning 重置为 0 时,CommandManager 仍然调用旧视图模型的 CanExecute 命令,所以我的 警告 属性再次获得 1 的值。

我的视图都是在资源字典中定义了 DataTemplates 的 UserControls,我能够设法解决这种行为的唯一方法是通过背后的 UserControl 代码,如下所示:

private void UserControl_Unloaded(object sender,System.Windows.RoutedEventArgs e)
{
     this.DataContext = null; //
}

问题:如何在 MVVM 中正确处理这种情况? 底线是我想在我的视图中跟踪属性更改,同时仍然能够通知用户未保存的更改如果他想离开这个相同的观点。

编辑:不确定如果有帮助,但这里也是我的命令实现:

    public class MyCommand : ICommand
    {
        private readonly Action<object> _execute;
        private readonly Predicate<object> _canExecute;

        public MyCommand(Action<object> execute,Predicate<object> canExecute)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecute == null)
            {
                return true;
            }
         
            return _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute?.Invoke(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
            

        public void RaiseCanExecuteChanged()
        {
          CommandManager.InvalidateRequerySuggested();
        }
    }

Defined DataTemplates(资源字典,在app.xaml中注册):

    <!--Each view has It's own reference of viewmodel-->
    <DataTemplate DataType="{x:Type viewmodels:Homeviewmodel}">
        <Views:HomeView />
    </DataTemplate>
    //...etc..

用于更改视图的属性

        ///<summary>Inherited Base property</summary>
        public object Open_view
        {
            get { return _open_view; }
            set { _open_view = value; OnPropertyChanged(); }
        }
        private object _open_view;

所有视图都通过 ContentControl 在 MainWindow 中打开(在此处也尝试使用 UpdateSourceTrigger ..):

 <ContentControl Grid.Column="1" Grid.Row="2"  Content="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}">

解决方法

这听起来像是一个绑定问题。具体来说:View(用户控件)不知道 ViewModel 发生变化的地方。

如果父 ViewModel 中的 ViewModel 可以更改,请确保添加 PropertyChanged 事件以通知 View 其绑定的 ViewModel 已更改。

过去在代码隐藏中设置数据上下文让我很头疼,所以我建议不要这样做:(this.DataContext = null)这样做(this.DataContext = ViewModel ).

所以确保 Open_view 看起来像这样:

public SomeViewModel Open_view 
{ 
  get { return _open_view; }
  set { _open_view = value; OnPropertyChanged(nameof(Open_view)); }
}

在父视图(保存用户控件的视图)中,像这样绑定数据上下文:

<SomeUserControl ... DataContext={Binding Path=Open_view,Mode=OneWay,UpdateSourceTrigger=PropertyChanged} ... />

当然需要把路径改成实际路径。

,

就我这几天的研究而言,不幸的是,没有任何简单的选项可以用来解决我的问题。发现的问题 herehere 更详细地解释了我的问题是什么。链接中的第一个问题实际上提供了一个可以完成且有用的答案,但在我的情况下需要大量编码。

然后我重新考虑了这个问题,并决定使用 Icommand CanExecute 放弃所有 UI 逻辑。即使在 UserControl Unloaded 事件上将 DataContext 设置为 null 也会导致我编写大量代码。

所以我决定尝试不同的方法,在我看来最简单的方法。我所做的是我创建了一个自定义按钮控件,它侦听IsEnabledChangedEvent,并将它的启用属性状态绑定到 ViewModel 属性。所以完整的代码是这样的:

静态属性:

public static class Global
{
   public static int Warning
   {
       get { return _warning; }
       set { _warning = value; OnStaticPropertyChanged(); }
   }
   private static int _warning;
}

自定义按钮:

public class Button_cancel : Button
{
    public Button_cancel()
    {
        //Custom style
        Style stil = FindResource("Btn_style") as Style;
        Style = stil;

        Width = 80;
        IsEnabledChanged += Button_IsEnabledChanged;
    }
  
    public static DependencyProperty ChangesProperty =
            DependencyProperty.Register("Changes",typeof(bool),typeof(Button_cancel),new FrameworkPropertyMetadata(false,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
           
     public bool Changes
     {
         get { return (bool)GetValue(ChangesProperty); }
         set { SetValue(ChangesProperty,value); }
     }
         
     private void Button_IsEnabledChanged(object sender,System.Windows.DependencyPropertyChangedEventArgs e)
     {
       Changes = IsEnabled ? true : false;
     }
}

ViewModel 属性:

public bool Changes_made
{
    get { return _changes_made; }
    set { 
           _changes_made = value;
           OnPropertyChanged();

           if (Changes_made)
           {
               Global.Warning = 1;
           }
           else
           {
               Global.Warning = 0;
           }
        }
 }
 private bool _changes_made;

XAML 绑定:

  <ctrls:Button_cancel Content="Cancel" Command="{Binding CANCEL}" Changes="{Binding Changes_made}" />
                       
   

最后,主窗口 ViewModel - 切换视图的方法(与之前相同):

private void Open_view(object parameter)
{
   if (Global.Warning != 0)
   {
       var msg = _windowservice.ShowMessage("Want to leave? Changes won't be saved.");

       if (msg == true) //User clicked OK in MessageBox
       {
          Global.Warning = 0; // reset static property to default,and proceed with code
       }
       else
       {
           return; //Prevent opening new view
       }
               
    }

    //Switch to new view
    Open_view = new SomeViewModel();

    //...
}

就像一个魅力,不会干扰 ICommand's CanExecuteChanged 事件。 因此,每当 ICommand 的 CanExecute 触发并启用 Button 时,我都会将此状态存储在另一个属性中。这样我就知道用户所做的每项更改。

我心中还有一个解决方案 - 跟踪每个必需属性设置器的更改,但这也涉及大量编码。

希望它对未来有所帮助,非常感谢您的帮助!