Thursday, January 6, 2011

NetMonitor. Bringing together UI and services

In the previous part I introduced a Ping service which purpose was to execute Ping command asynchronously, grab results and notify its listeners. Today I’m going to bring together the UI that had been constructed earlier with this service.

First of all, let’s connect a class that represents a Host with an instance of Ping service. Here is how HostViewModel was initially designed:

  public class HostViewModel : ViewModelBase
  {
    private string name;
    private string health;
    private string responseTime;

    public string Name
    {
      get { return name; }
      set {
        name = value;
        RaisePropertyChanged("Name");
      }
    }

    public string Address { // The same logic }

    public string Health {  // The same logic }

    public string ResponseTime {  // The same logic }
  }

I’m adding a reference to an instance of Ping service in its constructor, an event listener and 2 methods to control host monitoring from outside:

  public class HostViewModel : ViewModelBase
  {
    // ...
    private readonly PingService ping;
    private readonly Queue messageLog;

    public void StartMonitor()
    {
      ping.Start();
    }

    public void StopMonitor()
    {
      ping.Stop();
      Invalidate();
    }

    private void PingCompleted(object sender, PingServiceMessage e)
    {
      if (e.Status != IPStatus.Success) {
        Health = e.ErrorMessage;
        Invalidate();
        return;
      }

      ResponseTime = GetAverageResponseTime(e).ToString("F1");
      Health = "OK";
    }

    private double GetAverageResponseTime(PingServiceMessage e)
    {
      messageLog.Enqueue(e);
      if (messageLog.Count > 5)
        messageLog.Dequeue();

      return messageLog.Select(m => m.RoundtripTime).Average();
    }

    private void Invalidate()
    {
      messageLog.Clear();
    }

    public HostViewModel(string address, string name)
    {
      Address = address;
      Name = name;
      messageLog = new Queue(8);
      ping = new PingService(Address);
      ping.PingCompleted += PingCompleted;
    }
  }
}

Note that I’ve also added a message queue to calculate average value of response time based on last 5 messages. This might help to avoid dramatic raises and falls of the variable.

The second step is to modify MainViewModel in order to initialize a collection of HostViewModels and bind commands to methods.

  public class MainViewModel : ViewModelBase
  {
    // ...
    private bool isMonitoring;

    public ICommand StartCommand
    {
      get {
        if (startCommand == null)
          startCommand = new RelayCommand(Start, () => !IsMonitoring);  // Binding command to methods
        return startCommand;
      }
    }

    public ICommand StopCommand
    {
      get {
        if (stopCommand == null)
          stopCommand = new RelayCommand(Stop, () => IsMonitoring);  // Binding command to methods
        return stopCommand;
      }
    }

    public bool IsMonitoring
    {
      get { return isMonitoring; }
      set {
        if (isMonitoring == value)
          return;

        isMonitoring = value;
        RaisePropertyChanged("IsMonitoring");
      }
    }

    // Starting all monitors
    private void Start()
    {
      IsMonitoring = true;
      foreach (var host in Hosts)
        host.StartMonitor();
    }

    // Stopping monitors
    private void Stop()
    {
      foreach (var host in Hosts)
        host.StopMonitor();
      IsMonitoring = false;
    }

    // Initializing a collection of hosts
    public MainViewModel()
    {
      Hosts = new ObservableCollection
        {
          new HostViewModel("192.168.1.1", "Wi-Fi Router"),
          new HostViewModel("10.1.8.2", "Gate"),
          new HostViewModel("10.0.0.66", "VPN Server"),
          new HostViewModel("10.0.0.1", "DNS Server"),
          new HostViewModel("google.com", "Google")
        };
    }

    public override void Cleanup()
    {
        // Clean up if needed
      if (IsMonitoring)
        Stop();

      base.Cleanup();
    }
  }

That’s it.
Note that in this step as well as in the previous one we don’t touch UI part at all. Nevertheless, the architecture of the application where WPF binding & commands are used, allows us to build an application where  ViewModels don’t know about UI, and Models don’t know about both ViewModels and UI. Good layered architecture, I’d say.

Here is the application. It works just fine, reacts on toolbar buttons’ clicks, refreshing the UI asynchronously without any freezing, using the resources of system ThreadPool:

NetMonitor

As usual, I’ve pushed the modifications to the public repository.

In the next post I’ll make the list of hosts configurable from outside the application code.

5 comments:

  1. Why did you declared ResponseTime as a string property. I definitely must be double, or may be nullable double.

    ReplyDelete
  2. That's just because the topic of WPF converters is a subject of one of the next posts, so the current code base of the project is not ideal in any sense.

    ReplyDelete
  3. Actually you don't need converter here, you can specify StringFormat in binding declaration (-:

    ReplyDelete