0
\$\begingroup\$

So I've been learning about MVVM and I had a problem. I often had to start a thread to download or do some longer task in background and when it is done change some properties so the UI updates. The problem is that the UI will update only if I changed it from the main thread. So I often had to create DispatchTimers to wait for the thread to finish and then execute some code in main. Well this is my solution to it.

With this class you can simply go to your ViewModel constructor and do something like this:

ViewModelEventHandler.RegisterEventListener("EventName", EventAction);

EventAction being an action that is executed when the event is raised. You can simply create an async Task that does its work in the background and it raises the event "EventName" and then EventAction() is executed in main thread. You just have to put new ViewModelEventHandler(); inside your main function.

I also use it a lot for communication between view models. You can raise an event in view model A and a completely unrelated view model B can execute code.

Here is my code:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;

namespace MVVM {
  public class EventData < T1, T2 > {
    public T1 eventState {
      get;
      set;
    }
    public T2 subs {
      get;
      set;
    }
  }

  class ViewModelEventHandler {
    // holds all events and their subscribed actions, the string is the event name, bool keeps track of if the event is currently raised, and List<Action> holds all the functions that will be executed when the event is raised
    private static Dictionary < string, EventData < bool, List<Action> >> eventList = new Dictionary < string, EventData <bool,List<Action> >> ();


    public ViewModelEventHandler() {
      DispatcherTimer timer = new DispatcherTimer();
      timer.Interval = TimeSpan.FromMilliseconds(1);
      timer.Tick += eventListen_Tick;
      timer.Start();
    }

    public static void RaiseEvent(string eventName) {
      try {
        eventList[eventName].eventState = true;
      }
      catch {}
    }

    public static void RegisterEventListener(string eventName, Action eventMethod) {
      foreach(var regEvent in eventList) {
        if (regEvent.Key == eventName) {
          eventList[eventName].subs.Add(eventMethod);
          return;
        }
      }

      eventList[eventName] = new EventData < bool, List < Action >> ();
      eventList[eventName].eventState = false;
      eventList[eventName].subs = new List < Action > ();
      eventList[eventName].subs.Add(eventMethod);

    }


    public static void RemoveEventListener(string eventName, Action eventMethod) {
      try {
        eventList[eventName].subs.Remove(eventMethod);
      }
      catch {}
    }

    public static void RemoveEvent(string eventName) {
      try {
        eventList.Remove(eventName);
      }
      catch {}
    }

    private void eventListen_Tick(object sender, EventArgs e) {
      foreach(var ev in eventList) {
        if (ev.Value.eventState) {
          ev.Value.eventState = false;
          foreach(Action eventSub in ev.Value.subs) {
            eventSub();
          }
        }
      }
    }

  }
}
\$\endgroup\$
2
  • \$\begingroup\$ Why not use Dispatcher.BeginInvoke? I'm not a big fan of events like this as it make the code flow harder to see what is happening. You can just await or even do a continguewith and then once done trigger the Dispatcher.BeginInvoke passing in the same method you would have added to your event. \$\endgroup\$ – CharlesNRice Aug 14 '20 at 14:09
  • \$\begingroup\$ Get rid of those empty try catch blocks. It's a terrible habit. If an exception happens you will want to know about it. Consuming the exception and doing nothing will not make the code resilient; after an exception the state of the application might be incomplete or corrupt. Could lead to cascading failure and data corruption while also being difficult to pinpoint the problem. \$\endgroup\$ – null Sep 16 '20 at 7:12
1
\$\begingroup\$

Your approach is wrong. It creates too complex code for a very simple and common problem. To use a sort of event aggregator to report progress is overkill.

First notice that you generally distinguish between IO bound tasks and CPU bound tasks. CPU bound tasks are executed on a background thread. The performance of CPU bound tasks can be improved by using multi threading. For example calculations are CPU bound.
CPU bound tasks should be executed in parallel using background threads.

Reading from a file or download data are IO bound tasks. The performance of IO bound tasks cannot be improved using multiple threads, but by improving the IO devices. For example a download's performance is not limited by available CPU resources, but transfer rate or other network conditions and hardware. Using a thread for IO bound tasks is a waste of resources, since IO bound operations generally require almost no CPU resources.
IO bound tasks should be executed asynchronously or event-driven.

This means you should check the library you are using to download resources for asynchronous methods and await them e.g. await DownloadAsync(). Alternatively subscribe to corresponding API events e.g. DownloadProgressChanged. Every modern UI related API exposes asynchronous methods.

Since all DispatcherObject objects are associated with a Dispatcher and each thread has its own Dispatcher, you can't use or access a DispatcherObject on a different thread then the thread the DispatcherObject was created on. Except the DispatcherObject derives from Freezable and is in a frozen state.
To access a DispatcherObject from a different thread you must use the associated Dispatcher.

The following examples show how to update e.g., a Progressbar from a background thread:

MainWindow.xaml

<Window>
  <Progressbar x:Name="CompressionProgressbar" />
</Window>

MainWindow.xaml.cs

private async Task CompressData()
{
  // Start a background thread
  await Task.Run(() =>
  {
    // Access the DispatcherObject from a background thread 
    // using the associated Dispatcher
    this.Dispatcher.InvokeAsync(() => this.CompressionProgressbar.Value = 33.33);
  });
}

Since .NET 4.5 the recommended approach is to use IProgress<T> to report progress (or delegate data back to the UI thread). The library offers a ready to use implementation, the Progress<T> class. The constructor captures the current SynchronizationContext to post the registered callback to it. Therefore it is important to create the instance on the UI thread and then pass it to the background thread.

private async Task CompressData()
{
  // Create an instance of Progress<T> using the constructor
  // to register the callback which will be executed on the current thread e.g. to report the progress
  var progressReporter = new Progress<double>(progressValue => this.CompressionProgressbar.Value = progressValue);

  // Start a background thread
  await Task.Run(() =>
  {
    // Access the DispatcherObject from a background thread 
    // using the IProgress<T> instance
    progressReporter.Report(33.33);
  });
}
\$\endgroup\$
3
  • \$\begingroup\$ A reference for CPU/IO-bound - Asynchronous Programming. \$\endgroup\$ – aepot Dec 2 '20 at 22:13
  • \$\begingroup\$ I don't recommend Dispatcher because it makes code weird (unless the code is fully SRP-designed but it's pretty rare thing). IProgress is best, snd as slternative some Producer/Consumer implementation, especially modern async one e.g. IAsyncEnumerable or System.Threading.Channels. \$\endgroup\$ – aepot Dec 2 '20 at 22:18
  • 1
    \$\begingroup\$ I agree. I also recommend the IProgress<T> over the Dispatcher (see my answer). I have added the Dispatcher example for completeness. The fact that the Dispatcher (for the main STA thread) is globally available via the static Application.Current reference can lead to bad code design. But there are situations where the Dispatcher provides more flexibility. It allows to dispatch delegates using a priority, which can be handy at times. Channels are quite efficient, but they don't solve the problem of referencing DispatcherObject instances across threads. \$\endgroup\$ – BionicCode Dec 3 '20 at 13:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.