2

I'm working on a WPF application which has many user interactions which start with opening a non model window, allowing some interaction with the window and the rest of the application (think previewing changes, etc) and end when the window is closed. There is also some reputative setup and tear down which I want done at the beginning and end of these actions.

So I have a lot of functions like this:

private void DoOperation()
{
   // some setup process
   RepetativeSetUp();
   Window w = BuildWindow();
   w.Closing += (o, e) => 
   { 
      // ...Handle result of window...
      
      // some tear down process
      RepetativeCleanUp();
   }
   // Show the window non modal
   // PROBLEM!! this function continues immediately after this line!
   // and does not wait for the window to close.
   // w.ShowDialog is not an option because I do need that non modal behavior 
   w.Show();
}

I want to move this repetitive setup/teardown process out of each of these functions and off to some other process. My issue being that there isn't really any single function which encapsulates the "Operation". Typically to solve this type of problem I would do something like:

// I know there are a lot of ways to implement this type of pattern
// It is probably not how I would approach this in a larger code base
// but it is about as simple as I can make it
public static void SetUpCleanUp(Action operation)
{
    RepetativeSetUp();
    operation.Invoke();
    RepetativeCleanUp();
}

private void DoOperation_Simplified()
{
   Window w = BuildWindow();
   w.Closing += (o, e) => 
   { 
      // ...Handle result of window...
      // we want CleanUp here
   }
   w.Show();
   // not CleanUp here
}

// Not good:
// CleanUp is called too soon
SetUpCleanUp(DoOperation_Simplified);

So I came up with this:

public static async Task ShowAsync(this Window w)
{
    // I'm sure there is a more elegant way to handle the generic here, and actually have 
    // this task return a meaningful "dialog result", but for now the int generic can be ignored.
    TaskCompletionSource<int> tcs = new();
    Task<int> t1 = tcs.Task;
    w.Closing += (o, e) =>
    {
       tcs.SetResult(1);
    };

    w.Show();
    await t1;
}

// and now DoOperation becomes this, which is much easier to work with:
private async Task DoOperation_simplified2()
{
   Window w = BuildWindow();
   await w.ShowAsync();
   // Handle result of window...
}

// and now I'm going to gloss over some of the complication that my SetUpCleanUp 
// now has to be async and handle awaiting it's action... 
// but I got what I wanted, a single function encompassing my operation? 
SetUpCleanUp(DoOperation_Simplified2);

My questions are:

  • Am I reinventing the wheel here? I can't really find anything online talking about this type of problem but I can't be the first person to struggle with the issue? Is there an easier way to approach this problem?
  • This feels like it might to balloon into a real nightmare to maintain. Has anyone gone down this path before? I don't want to trade one (sort of manageable) maintenance issue for another (maybe less manageable) one.
New contributor
LOul is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
7
  • Is OperationManager a class with static methods? Commented Nov 20 at 22:45
  • In my case OperationManager is a class instance shared between all things looking to do operations. Pascal casing was probably a poor choice in the example. Commented Nov 20 at 23:16
  • I'm having a hard time understanding the problem. Why do the actions need to interact with the window at all? What are these actions? The user can do other things, so are these windows just there for informational purposes? Commented Nov 20 at 23:38
  • Can you edit your post to provide more information about the use case? I think I need more context to understand what the problem is, and what the solution could be . Commented Nov 20 at 23:39
  • Thanks for the questions. Hopefully my edits make things more clear. Commented 2 days ago

1 Answer 1

2

Am I reinventing the wheel here? I can't really find anything online talking about this type of problem but I can't be the first person to struggle with the issue?

I don't think this kind of use case is very common. Usually the window itself would be responsible for any setup and teardown if such is needed. So I'm not all that surprised that there is a lack of resources.

Is there an easier way to approach this problem?

Either of your options looks fine to me. I would not expect to use this pattern all that often, so I would not worry to much about making it super neat. Just make sure to provide comments to explain what problem the code aims to solve.

If you want to provide a meaningful result you could use generics with a constraint. And there is no need to use async in the helper method. I think something like this should work:

public static Task<TResult> ShowAsync<TWindow, TResult>(this TWindow w, Func<TWindow, TResult> getResult) where TWindow : Window
{
    TaskCompletionSource<TResult> tcs = new();
    w.Closing += (o, e) =>
    {
       if(w.DialogResult == true){
          tcs.SetResult(getResult(w));
       }
       else{
          tcs.SetCanceled(); // Make sure to catch OperationCancelledException
       }       
    };

    w.Show();
    return tcs.Task;
}

This feels like it might to balloon into a real nightmare to maintain. Has anyone gone down this path before?

Using asynchronous programming will add some complexity, but I would not call it a maintenance nightmare. While this kind of thing is not the main use case of asynchronous programming, it can still be a useful pattern.

Not quite the same, but I have used TaskCompletionSource as a way to build state machines before, where the user needs to perform a series of specific actions in sequence. This added a fair bit of complexity in all the helper classes, but the code for the state machine is fairly easy to read since it is almost like regular synchronous code. I prefer this over the alternative of writing out the state machine as a huge switch statement myself.

But this is a discussion you should have in your team. Things like pattern and coding style can be very opinion based, and it is sometimes more important to have a cohesive style that you all agree with than to use the best possible pattern for a given situation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.