There is an application with a set of periodically opening / closing tabs on the tape. The main view model contains information about the list of tabs, the selected tab and the command for creating new tabs:

public sealed class MainVm : ViewModel { // создает новую вкладку, добавляет ее в коллекцию и делает выбранной public ICommand OpenNewTabCommand { get; } public ObservableCollection<TabVm> Tabs { get; } public TabVm SelectedTab { get; set; } // PropertyChanged } 

The tab view model provides the name and command to close the tab:

 public sealed class TabVm : ViewModel { private bool isClosed; private void HandleClose() { Parent.SelectedTab = Parent.Tabs.FirstOrDefault(_ => _ != this); Parent.Tabs.Remove(this); isClosed = true; } private bool CanClose() { if (isClosed) { Trace.WriteLine($"{Name}: вызван CanClose после закрытия вкладки"); } return true; } public MainVm Parent { get; } public string Name { get; } public ICommand CloseCommand { get; } public TabVm(MainVm parent, string name) { Parent = parent; Name = name; CloseCommand = new RelayCommand(HandleClose, CanClose); } } 

ICommand implementation:

 public sealed class RelayCommand : ICommand { private readonly Action execute; private readonly Func<bool> canExecute; public RelayCommand(Action execute, Func<bool> canExecute) { this.execute = execute; this.canExecute = canExecute; } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public bool CanExecute(object parameter) { return canExecute(); } public void Execute(object parameter) { execute(); } } 

Main window view:

 <RibbonWindow ...> <Ribbon ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}"> <Ribbon.ApplicationMenu> <RibbonApplicationMenu> <RibbonApplicationMenuItem Header="Новая вкладка" Command="{Binding OpenNewTabCommand}"/> </RibbonApplicationMenu> </Ribbon.ApplicationMenu> <Ribbon.TabHeaderTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}"/> <Button Content="Close" Command="{Binding CloseCommand}"/> </StackPanel> </DataTemplate> </Ribbon.TabHeaderTemplate> </Ribbon> </RibbonWindow> 

If I create a new tab, and then close it, then in the Output there will be messages about calling CanClose for the already closed tab. All of this will continue until the garbage collector reaches the tab view model. This is probably due to the subscription to the RequerySuggested event of the CommandManager in the command.

Are there any suggestions for circumventing this situation?

One option is to make the command Disposable. Within the team itself, collect subscribers into the list, and in Dispose, unsubscribe. Not a good option, because you have to either pull Dispose through reflection automatically, or watch it all and release it manually.

  • And what's wrong with that? CanExecute can call both before and after display. This is expected behavior in WPF. - Monk
  • @Monk, the bad thing is that in some cases it climbs to an already released context, resulting in an ObjectDisposedException. - Vlad
  • one
    In most cases it is easier to check all this in CanExecute, because it is his responsibility. Or, CanExecuteChanged should not cling to the CommandManager, but only to events that are clearly related to it personally. - Monk
  • @Monk, well, yes, as an option, catch Exception in CanExecute. Not clinging to the CommandManager is not an option, since then you have to do a lot more body movements. - Vlad
  • So that's the whole point. Either you manually drive accessibility, or do not complain about late destruction and incorrect calls. - Monk

1 answer 1

The workarounds for this situation that I came up with (for Disposable presentation models):

  1. Disposable command. We collect subscribers, and in Dispose we unsubscribe.
  2. Swallow all exceptions in CanExecute.
  3. Pass the view model to the command. Inside the command to determine that the view model should no longer exist and return false.

I stopped at option number 3.