0

I have a static property which tracks property changes in my viewmodels:

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

Then, in viewmodels I have CanExecute commands (for buttons to cancel changes):

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

When there are changes in my views, and user want to switch a view, I want to show them a warning MessageBox. So in my main Window viewModel I have a command for changing views:

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();

    //...
}

But, when I confirm MessageBox to leave view without saving changes & with reseting static property Warning to 0, CommandManager still invokes CanExecute command of old viewmodel, so my Warning property gets again value of 1.

My views are all UserControls with defined DataTemplates in resource dictionary, and only way I could manage to solve this behaviour is by UserControl code behind, like this:

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

Question: How to properly handle this situations in MVVM ? Bottom line is that I want to track property changes in my view while still be able to inform user of unsaved changes If he wants to leave this same view.

EDIT: not sure If helpful, but here is my command implementation too:

    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 (resource dictionary, which is registered in app.xaml):

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

Property for changing views:

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

All views are opened in MainWindow via ContentControl (tried with UpdateSourceTrigger here too..) :

 <ContentControl Grid.Column="1" Grid.Row="2"  Content="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}">
Lucy82
  • 654
  • 2
  • 12
  • 32
  • Can you add part of the Xaml of the parent and the Xaml of the user control itself? Also the parent viewmodel would be of great help to help find the problem. – Cédric Moers Apr 29 '21 at 13:19
  • Move the code with check from `Cancel_CanExecute` to the `Cancel_Command` itself, then it will be called only once. – Rekshino Apr 29 '21 at 13:21
  • @CédricMoers, I think that posting code here would be a bit overwhelming or even against SO rules. There is just too much of It all together - a lof ox xaml and classes inheriting each other. – Lucy82 Apr 29 '21 at 13:28
  • @Rekshino, how exactly did you mean that ? My command is set like this in viewmodel: `CANCEL = new MyCommand(Cancel_Execute, Cancel_CanExecute);`. **CANCEL** is property of type `MyCommand`. – Lucy82 Apr 29 '21 at 13:32
  • Can you post the part of the code where you are assigning the viewmodel to the view? Either by binding or in code? Also how you define your Open_view property. Please see my anwer for a clarification of what I mean. – Cédric Moers Apr 29 '21 at 13:33
  • Move the check to the `Cancel_Execute` – Rekshino Apr 29 '21 at 13:34
  • @CédricMoers, done in edited question, at bottom. – Lucy82 Apr 29 '21 at 13:42
  • @Rekshino, stiil don't see a point. If I move check to `Cancel_Execute`, then **Warning** property will only be set If I actually click on Cancel button - so changes will be reset by user, in a view. Why would then user want to be warned of unsaved changes when switching View, If he allready cancelled them ? – Lucy82 Apr 29 '21 at 13:46
  • Thank you, now I need to know where the DataTemplate is shown (i.e. the parent), this can be a ContentPresenter or something similar. The ContentPresenter its DataContext should be bound to the Open_view property. Can you also add this part? – Cédric Moers Apr 29 '21 at 13:48
  • @Lucy82: You should set the `Global.Warning` property in the `CanExecute` method since you cannot really control when this method is invoked. – mm8 Apr 29 '21 at 13:49
  • @CédricMoers, my views are shown via ContentControl, have done this in edit too. ContenControl is part of MainWindow xaml. – Lucy82 Apr 29 '21 at 13:57
  • @mm8, I do set `Global.Warning property` in `CanExecute` method of viewmodel. Did you mean something else - `CanExecute` of `MyCommand` ? – Lucy82 Apr 29 '21 at 13:59
  • How am I or anyone else supposed to know how your `CanExecute` method is invoked given the code you have posted? – mm8 Apr 29 '21 at 14:51

2 Answers2

0

This sounds to me like a binding issue. In specific: where the View (User control) is not aware that the ViewModel changes.

If the ViewModel in a parent ViewModel can change, make sure to add a PropertyChanged event to notify the View that the ViewModel it binds to has changed.

Setting the datacontext in code-behind gave me a lot of headaches in the past, so I suggest not doing this: (this.DataContext = null) nor doing this (this.DataContext = ViewModel).

So make sure that Open_view looks something like this:

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

And in the parent view (the view that holds your user control) bind the data context like so:

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

Of course, you need to change the path to the actual path.

Kit
  • 20,354
  • 4
  • 60
  • 103
Cédric Moers
  • 395
  • 1
  • 10
  • @Kit, tried that too, no success. My viewmodels, as I mentioned are defined in resource dictionary, so when I set `Open_view` to new viewmodel my view changes automatically via defined DataTemplate, no need to define datacontext in xaml. And My `Open_view` allready had PropertyChanged defined. – Lucy82 Apr 29 '21 at 13:36
  • Is the UpdateSourceTrigger set to PropertyChanged? Perhaps the path is wrong. The way I interpret your situation, the view its data context is not updated with the newly set ViewModel. So probably a binding issue. You can try Snoop to find these kind of issues, or depending on your VS version, VS can help you analyze the databinding issues. With the given context, I fear this is all I can help. – Cédric Moers Apr 29 '21 at 13:40
  • @CedircMoers, maybe, I'm clueless of what's going on. My views work fine with every binded properties, and all commands work perfectly. So I think Datacontext is not an issue. Problem is probably that datacontext doesn't change to 'Open_view' property. However, breakpoint on property **set** clearly shows that I changed property with swiching view. – Lucy82 Apr 29 '21 at 13:52
  • @Lucy82 with the added context I see that you are using Content, not DataContext. Please try adding; DataContext="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}" to the ContentControl. You can keep the Content="{Binding Open_view,UpdateSourceTrigger=PropertyChanged}" as well. But you can also try removing the Content=... and see if that works. – Cédric Moers Apr 29 '21 at 14:00
  • But I think you should do both Content and DataContext, not sure however... – Cédric Moers Apr 29 '21 at 14:02
  • From what I can find, it seems that only content should do, so perhaps this isn't the problem. But you can give it a try. Have you checked the binding errors? – Cédric Moers Apr 29 '21 at 14:07
  • It could also be that I am on the wrong track. I see the cancel command as being part of the Open_view. If this is not the case, and the cancel command is part of the parent, then -sadly- I'm pointing you in the wrong direction. – Cédric Moers Apr 29 '21 at 14:18
  • I have tried various things, but none approach except one posted in my question works. My ViewModel just doesn't get disposed in same moment as I switch View. However, I found out that I wasn't the only one with problems like this with CanExecute of command. – Lucy82 Apr 29 '21 at 18:39
0

As far as I researched these couple of days, there aren't any easy options that I could use to solve my issue, unfortunally. Questions found here and here explain a bit more what my problem is. First question in link actually provides an answer that could be done and is useful, but would aquire a lot of coding in my situation.

Then I re-thinked all over this issue and decided to drop off any UI logic with Icommand CanExecute. Even with setting DataContext to null on UserControl Unloaded event as I mentioned in question would lead me to write a lot of code.

So I decided to try different aprroach, in my opinion most easiest one. What I did is that I created a custom Button control, that listens to IsEnabledChangedEvent, and bind It's state of enabled property to ViewModel property. So complete code looks like this:

Static property:

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

Custom button:

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 property:

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 binding:

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

And finally, Main window ViewModel - a method where views are switched (same as before):

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();

    //...
}

Works like a a charm, without interfering ICommand's CanExecuteChanged event. So, whenever ICommand's CanExecute fires and enables Button, I store this state in another property. This way I know for each change that has been made by user.

There is one more solution in my mind too - tracking changes in each of required property setters, but that also involves a lot of coding.

Hope It helps someone in future, and many thanks for all your help !

Lucy82
  • 654
  • 2
  • 12
  • 32