0

I have created an ICommand that apart from executing some SQL procedures, it checks whether or not the MainWindow UI has been closed while the SQL procedures are executing. In case the MainWindow is closed by the user, the I want the application to simply stop by using the property return;.

My problem is that the ICommand is inside an MVVM model. And this MVVM model is outside the MainWindow (Window class). So, when I try to call the base.OnClosed I get an error that I cannot access it because of its protection level (Window methods are either protected or private).

How my code implementation looks like:

namespace Project
{
    class ClosedClass //I turned it into a class because methods cannot be accessed outside the Window Class
    {
        public bool IsClosed {get; private set;}
        protected override void OnClosed (EventArgs e)
        {
            base.OnClosed(e); //Error: object does not contain a definition for 'OnClosed'
            //(Approach 2) MainWindow.OnClosed(e); //Error: is inaccessible due to its protection level
            
            IsClosed=true;
        }
    }
    
    public class MainWindowViewModel: INotifyPropertyChanged
    {
        public ICommand RunCommand
        {
            get {retun new DelegateCommand<object>(FuncRunCommand);)
        }
        public async void FuncRunCommand(object parameters)
        {
            await Task.Run(() => RunCustomMetod()); //inside this method lies the IsClosed statement
        }
        
        public void RunCustomMetod()
        {
            if(IsClosed) //Error: The name 'IsClosed' does not exist in the current context
            {
                return; //this stops the execution of the RunCustomMetod() if IsClose is true
            }
        }
    }
    public partial class MainWindow : Window
    {
        //... 
    }
}

Based on some research I did for similar questions I found out those two answers:

However, my inexperience with C# did not help me to understand how to solve my problem even though the answers were clear. Any suggestions? I appreciate your time and effort in advance.

[UPDATE -- based on the comments]

namespace Project
{
    public class MainWindowViewModel: INotifyPropertyChanged
    {
        
        public bool IsClosed {get; private set;}
        protected override void OnClosed(EventArgs e) //1.Error: MainWindowViewModel.OnClosed(EventArgs)': no suitable method found to override

        {
            MainWindow.OnClosed(e) //2.Error: Inaccessible due to its protection level
            ((MainWindowViewModel)DataContext).IsClosed = true; //3.Error: 'DataContext' does not exist in the current context.
        }

        public ICommand RunCommand
        {
            get {retun new DelegateCommand<object>(FuncRunCommand);)
        }
        public async void FuncRunCommand(object parameters)
        {
            await Task.Run(() => RunCustomMetod()); //inside this method lies the IsClosed statement
        }
        
        public void RunCustomMetod()
        {
            if(IsClosed) //Error: The name 'IsClosed' does not exist in the current context
            {
                return; //this stops the execution of the RunCustomMetod() if IsClose is true
            }
        }
    }
    public partial class MainWindow : Window
    {
        //... 
    }
}
DXser1
  • 25
  • 5
  • Not according to the pure MVV; doctrine, but you could do the following in `MainWindow.OnClosed`: `((MainWindowViewModel)DataContext).IsClosed = true;` – Klaus Gütter Nov 25 '20 at 13:27
  • Look also this https://stackoverflow.com/questions/3683450/handling-the-window-closing-event-with-wpf-mvvm-light-toolkit – apomene Nov 25 '20 at 13:29
  • @KlausGütter Based on your approach ```DataContext``` is not defined so I get an error. Does your answer need something more to be complete? – DXser1 Nov 25 '20 at 13:59
  • @apomene I have searched for this article before however, I don't want to call the ```Closing``` event inside ```MainWindow()``` class whatsoever. I want the custom method inside the MVVM model to call it. Did I miss something in your posted link? If so it would really help me if you post an answer based on my code. – DXser1 Nov 25 '20 at 14:00
  • *I want the custom method inside the MVVM model to call it [the Closing event]* This does not make sense, the event is raised by a user action, not the model. – Klaus Gütter Nov 25 '20 at 14:17
  • *DataContext is not defined*: did you add the line to your [OnClosed](https://learn.microsoft.com/en-us/dotnet/api/system.windows.window.onclosed) override in MainWindow? – Klaus Gütter Nov 25 '20 at 14:19
  • @KlausGütter 1) *did you add the line to your OnClosed override in MainWindow*: Yes 2) *the event is raised by a user action*: Yes the even is raised by a user action. And when raised I want the RunCustomMetod() called by the ICommand to stop executing – DXser1 Nov 25 '20 at 14:27
  • 1) And did you add the IsClosed property to your viewModel? What is the error message you get? – Klaus Gütter Nov 25 '20 at 14:39
  • @KlausGütter check my update :) – DXser1 Nov 25 '20 at 14:50
  • You added the OnClosed to the MainWindowViewModel instead of the MainWindow. – Klaus Gütter Nov 25 '20 at 14:55
  • @KlausGütter yeah because the ICommand exists in the ViewModel not the MainWindow. I surely miss something here (meaning something it's not clear to me). The function ```RunCustomMetod()``` is called inside an ICommand of the MVVM model – DXser1 Nov 25 '20 at 14:58
  • But you close the *window*, therefore you have to start there and then inform the ViewModel that something happened. Either with my somewhat dirty approach or with the cleaner command-based one linked by @apomene – Klaus Gütter Nov 25 '20 at 15:01
  • @KlausGütter basically what I want is to find a way to check if a Window is closed and then stop the execution using ```return```. Hope it's more clear now – DXser1 Nov 25 '20 at 16:44

1 Answers1

1

This is MVVM, so you don't want the view model to care about any state of the UI. Also, you never want the view model to depend on methods defined in a view class.
If the view model depends on properties, use data binding to send the value to the view model, but avoid to introduce properties just to observe the view (as this is most of the time a hint for a bad design).

What you obviously really want is to allow the view to cancel the executing command (or more precisely the background thread) based on the UI state or a user interaction.

The recommended approach in such a scenario is to use the simple Task cancellation pattern using a CancellationToken.

When the window closes, you invoke a CancellationTokenSource.Close() method. Generally all Task library API methods support cancellation. Simply chose the appropriate overload that accepts a reference to a Cancellationtoken.

To provide a cancellation mechanism for custom methods, you simply have to pass around a reference to the actual CancellationToken (created by a CancellationTokenSource and call CancellationToken.ThrowIfCancellationRequested() to make the CancellationToken throw an OperationCancelledException in case CancellationTokenSource.Close() was invoked (alternatively poll CancellationToken.IsCancellationRequested):

MainViewModel.cs

public class MainViewModel : INotifyProeprtyChanged
{
  public MainViewModel()
  {
    this.CancellationTokenSource = new CancellationTokenSource();
  }

  private void CancelSql(object obj)
  {
    this.CancellationTokenSource?.Cancel();
  }

  private async void ExecuteSqlAsync(object obj)
  {
    // The complete Task API accepts a CancellationToken to allow cancellation.
    try
    {
      await Task.Run(() => RunCustomMethod(this.CancellationTokenSource.Token), this.CancellationTokenSource.Token);
    }
    catch (OperationCanceledException e)
    {
      // Do some optional cleanup before recovering from the exception

      // Once cancelled, we have to use a new CancellationTokenSource instance. 
      // But first, we have to dispose the old one
      this.CancellationTokenSource.Dispose();
      this.CancellationTokenSource = new CancellationTokenSource();
    }
  }

  private void RunCustomMethod(CancellationToken cancellationToken)
  {
    // Test if operation has already been cancelled.
    // This stops the execution of the current method, if the CancelSqlCommand was executed.
    cancellationToken.ThrowIfCancellationRequested();

    // Continue execution. Periodically call cancellationToken.ThrowIfCancellationRequested()
    // whenever possible to allow cancellation of the current operation.

    // Dummy loop to create a executable example
    while (true)
    {
      // Periodically check if the operation has been cancelled
      cancellationToken.ThrowIfCancellationRequested();
    }
  }

  public ICommand AbortSqlCommand => new DelegateCommand<object>(CancelSql);
  public ICommand ExecuteSqlCommand => new DelegateCommand<object>(ExecuteSqlAsync);
  private CancellationTokenSource CancellationTokenSource { get; set; }
}

SomeWindow.xaml.cs

public partial class SomeWindow : Window
{
  public SomeWindow()
  {
    InitializeComponent();

    this.DataContext = new MainViewModel();
    this.Closing += CancelSqlOperationsOnClosing;
  }

  private void CancelSqlOperationsOnClosing(object sender, CancelEventArgs e)
  {
    if (this.DataContext is MainViewModel viewModel)
    {
      viewModel.AbortSqlCommand.Execute(string.Empty);
    }
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thank you for the answer Bionic. Breifly, I want to check whether a Window is closed. And if so termincate the async task as you imply. I will check your answer and let you know – DXser1 Nov 25 '20 at 16:47
  • I understood your point. But your approach is totally wrong. Don't let the view model *ask* the window whether it is closed. Let the window *tell* the view model that it is closing/closed. In MVVM the communication between view and view model is unidirectional, from view to view model. In this relation the view is the active part (it knows the view model) and the view model is always the passive part (doesn't know the view). The above example shows a way to let the view tell the view model that it is about to close, so that the view model can abort any running operations. – BionicCode Nov 25 '20 at 16:53
  • Note that you are creating sort of a race condition.It is very likely that the window is never closed before the `RunCustomMetod()` is executed. In case you want to prevent this method from executing (instead of aborting it), then your implementation will fail. The method will execute before the user is able to close the window - the user is too slow. In case you just want to cancel the background task, then you should know that since this is a child task attached to the foreground task, it automatically abort when the foreground thread (UI thread) is aborted e.g. by closing the application. – BionicCode Nov 25 '20 at 17:01
  • 1
    nice this is a very good explanation and I am really glad you gave such an example. I am learning about C# and WPF so MVVM was the way to go. This is why I am developing my first app based on this pattern. Thanks a lot. I will let you know about the results – DXser1 Nov 25 '20 at 17:02
  • As far as I know there are two concepts: 1) App.Shutdown, 2) Window.Close. I want the second concept to be executed and stop (abort) the execution of the ICommand. In the second concept the application runs but the MainWindow closes. Am I right? – DXser1 Nov 25 '20 at 17:05
  • I was just saying, because in your example you were using the MainWindow. So I assume that this is the last/only window. By default `Application` will shutdown when the last window is closed. So if `MainWindow` is the only/last window, then the application will shutdown. To change this behavior you can set the [`Application.ShutdownMode` property](https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.shutdownmode?view=net-5.0). – BionicCode Nov 25 '20 at 17:10
  • *So if MainWindow is the only/last window* No it won't be the last open window. If the MainWindow closes, the user is prompt to the LoginScreen Window were (s)he can enter his credential to re-login to MainWindow. But I got your point thnx :) The part you said the let the View tell the ViewModel that it's closed blew my mind. – DXser1 Nov 25 '20 at 17:12
  • In this case the above solution will do exactly what you want. – BionicCode Nov 25 '20 at 17:14
  • BionicCode I have two comments to make about your answer: 1) the code: ```this.CancellationtokenSource.Dispose();``` I think should be ```this.CancellationTokenSource.Dispose();``` (with capital T). 2) When I call this ```public ICommand ExecuteSqlCommand => new DelegateCommand(ExecuteSqlAsync);``` I get an error that this command has a wrong return type. – DXser1 Nov 25 '20 at 21:06
  • @DXser1 Thank you. I fixed the errors. 1) Correct, that's a typo. 2) I don't know what library you are using. But I guess the `ICommand` implementation doesn't support asynchronous command delegates (where the delegate type is `Task`). I have changed the method type of `ExecuteSqlAsync`to `void`. This should fix it. Let me know if you have more issues. – BionicCode Nov 25 '20 at 22:05
  • Indeed the errors are fixed now. Some clarifications: 1) For the ```ICommand``` I use the ```Prism``` library. 2) You will notice in the code I posted that inside the method ```RunCustomMethod()``` I use this ```if(IsClosed){return;}``` to check if the window is closed and stop the program. Based on your solution I will now use this line ```cancellationToken.ThrowIfCancellationRequested();```. The latter checks if the window is closed and stops the program. Am I right? – DXser1 Nov 26 '20 at 07:55
  • @DXser1 To be precise: `cancellationToken.ThrowIfCancellationRequested();` will only check if the `CancellationTokenSource.Cancel()` was invoked. If `true`, the method will throw an exception that will cancel every operation. You have to catch this exception to recover the application and eventually roll back certain operations and clean up resources. If you don't like the exception driven way, you can poll the `cancellationToken.IsCancellationRequested` property and cancel manually. – BionicCode Nov 26 '20 at 08:12
  • In the above implementation the `Closing` event of the window will invoke the `CancellationTokenSource.Cancel()` via a `ICommand`. Since the cancellation is triggered by an `ICommand` you can use additional command sources like a cancel button in the view to allow the user to explicitly cancel the current operation. – BionicCode Nov 26 '20 at 08:13
  • 1
    Correct, `if(IsClosed){return;}` is replaced by `cancellationToken.ThrowIfCancellationRequested()`. Alternatively you can use `if(cancellationToken.IsCancellationRequested){return;}` – BionicCode Nov 26 '20 at 08:15