【问题标题】:Disable CanExecute on switching views在切换视图时禁用 CanExecute
【发布时间】:2021-07-22 20:25:09
【问题描述】:

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

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;
}

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

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 命令,所以我的 Warning 属性再次获得 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();
        }
    }

定义的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}">

【问题讨论】:

  • 能否添加部分父类的Xaml和用户控件本身的Xaml?此外,父视图模型对于帮助找到问题也很有帮助。
  • 将带有检查的代码从Cancel_CanExecute移动到Cancel_Command本身,然后它只会被调用一次。
  • @CédricMoers,我认为在这里发布代码会有点压倒性甚至违反 SO 规则。太多了 - 一个 lof ox xaml 和相互继承的类。
  • @Rekshino,你到底是什么意思?我的命令在 viewmodel 中是这样设置的:CANCEL = new MyCommand(Cancel_Execute, Cancel_CanExecute);CANCELMyCommand 类型的属性。
  • 您可以发布您将视图模型分配给视图的代码部分吗?通过绑定还是在代码中?还有你如何定义你的 Open_view 属性。请参阅我的回答以澄清我的意思。

标签: c# wpf icommand


【解决方案1】:

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

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

在代码隐藏中设置datacontext过去让我很头疼,所以我建议不要这样做:(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} ... />

当然,你需要将路径更改为实际路径。

【讨论】:

  • @Kit,也试过了,没有成功。正如我提到的,我的视图模型是在资源字典中定义的,所以当我将Open_view 设置为新的视图模型时,我的视图会通过定义的 DataTemplate 自动更改,无需在 xaml 中定义数据上下文。我的Open_view 已经定义了 PropertyChanged。
  • UpdateSourceTrigger 是否设置为 PropertyChanged?也许路径是错误的。我解释您的情况的方式是,视图的数据上下文不会使用新设置的 ViewModel 更新。所以可能是一个绑定问题。您可以尝试 Snoop 来查找此类问题,或者根据您的 VS 版本,VS 可以帮助您分析数据绑定问题。在给定的背景下,我担心这就是我能提供的全部帮助了。
  • @CedircMoers,也许,我不知道发生了什么。我的视图适用于每个绑定属性,并且所有命令都可以完美运行。所以我认为 Datacontext 不是问题。问题可能是 datacontext 没有更改为“Open_view”属性。但是,属性 set 上的断点清楚地表明我使用切换视图更改了属性。
  • @Lucy82 添加了上下文 我看到您使用的是 Content,而不是 DataContext。请尝试添加; DataContext="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}" 到 ContentControl。您也可以保留 Content="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}" 。但您也可以尝试删除 Content=... 看看是否可行。
  • 但是我认为你应该同时做 Content 和 DataContext,但是不确定......
【解决方案2】:

就我这几天的研究而言,不幸的是,没有任何简单的方法可以用来解决我的问题。发现的问题 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 时,我都会将此状态存储在另一个属性中。这样我就可以知道用户所做的每一项更改。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-04
    • 1970-01-01
    • 2014-01-04
    • 1970-01-01
    • 2011-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多