“Don’t shoot me, I’m just the In Process messenger”

In need of an English translation? Please drop me a note.

Voor ons huidige project zochten we naar een goed ontkoppeling tussen de user interface en de communicatie met hardware. Omdat het een WPF applicatie is en we gebruik maken van MVVM draait alles in hetzelfde applicatie.  De hardware willen we real time uitlezen en reageren op het gedrag.

Er werd dus nagedacht over het gebruik van Events. Dat is een prima oplossing voor “Loose coupling”. Maar hierbij is de ontkoppeling maar één kant op. De ‘luisteraar’ kent de verstrekker van de gebeurtenissen want hij moet zich daarop abonneren.

Omdat we MVVMLight gebruiken kregen we de mogelijkheid om nog verder te gaan met ontkoppeling. MVVMLight bezit namelijk ook een Messagebus.

De messagebus kennen we vooral vanuit SOA en Enterprise architectuur:

Maar hier hebben we te maken met messsages die In Process verstuurd worden. Hierbij zijn de volgende klassen te herkennen:

  1. De boodschap, de Message
  2. Diegene die ‘m verstuurt, de Sender
  3. Diegene die zich abonneert op dit type bericht, de Target of algemener, de Receiver
  4. De Messenger

Bij MVVMLight is dit opgelost met een aantal objecten die globaal beschikbaar zijn en via locking threadsafe zijn gemaakt. Hierdoor wordt het mogelijk om overal in de applicatie boodschappen te verzenden en eventueel te ontvangen.

Een boodschap is in het algemeen een overerving  van MessageBase, een standaard klasse beschikbaar in MVVMLight:

public class DeviceMessage : MessageBase
{
  public DeviceMessage(string message)
  {
    Message = message;
  }

  public string Message { get; private set; }

  public override string ToString()
  {
    return "Device message: " + Message;
  }
}

Hier hebben we een DeviceMessage die één property genaamd Message bezit.
Een message wordt verstuurd via de Send methode op de static Messenger:

Messenger.Default.Send<DeviceMessage>
    (new DeviceMessage("Time on device: " + DateTime.Now.ToString()));

Meer is er niet nodig om een boodschap op de Messagebus te zetten. In dit voorbeeld kan ik berichten met een datum/tijd verwachten. Zo kan ik ook vanuit een ViewModel juist een start- of stopcommando doorgeven aan een bepaald apparaat (of beter: een instanciering van iets wat de interface van dat apparaat implementeert). Ik heb geen kennis van de luisteraar, ik weet niet eens of die wel bestaat.

Hoe wordt het bericht ontvangen? Via een constructor of initialisatie heeft een andere partij zich geabonneerd op deze berichten:

Messenger.Default.Register<DeviceMessage>
  (this, true, m => {   Message = m.Message; });

In dit geval wordt de boodschap in op een ViewModel de m.Message uitgelezen en op het scherm getoond (via een bindable property).

Maar er is ook een bonus. De MessageBus is globaal beschikbaar en iedereen kan zich abonneren op berichten. MVVMLight biedt de mogelijkheid om met een Message ook een filtertoken op te nemen.

Messenger.Default.Send<SwitchMessage>
  (new SwitchMessage(this, "AnotherDevice"), "DeviceSwitchToken");

In dit geval kunnen meerdere partijen zich abonneren op een SwitchMessage. Maar door de meegestuurde “DeviceSwitchToken” zal wellicht niet iedereen de SwitchMessage verwerken, ze wachten op zo’n bericht vergezeld met een ander token.

En het is ook mogelijk om zich juist op een scala van berichten te abonneren. Het is mogelijk om alle overervingen van een baseclass te ontvangen:

public class LogProvider : ILogProvider
{
  public void Initialize()
  {
    // log all messages
    Messenger.Default.Register<MessageBase>(this, true, m =>
    {
      Debug.WriteLine(
        String.Format(
           "At {0} we received: {1}", DateTime.Now.ToShortTimeString(), m));
    });
  }
}

Hier heb ik een logger geabonneerd op alle overervingen van MessageBase.

Door de totale ontkoppeling van de zenders en ontvangers kan je het overzicht verliezen. Maar juist door deze logger heb ik een mooie tijdslijn van wat er onderling verstuurd is en kan ik analyseren wat er mis is gegaan. Daarom implementeer ik ook de ToString() zodat ik de messages hun eigen te loggen boodschap kan laten beschrijven.

Ik kan ook als een soort van watchdog een pulse afgeven. Als ik niet de gewenste antwoorden ontvang kan ik ‘alarm slaan’. En dit kan ik doen zonder dat ik de daadwerkelijke implementatie van de te controleren instanties niet ken.

Er zijn in MVVMLight nog een aantal andere boodschappen mogelijk. Ik wil er nog één aanstippen.

MVVMLight kent ook een generic message. Deze bevat standaard een Content property waarin een andere klasse gestopt kan worden. In bovenstaande voorbeelden is steeds gebruik gemaakt van een MessageBase overerving met extra properties. Met de generic oplossing kunnen die properties in de Content geplaatst worden. De Content zier er dan zo uit:

public class SwitchContent
{
  public SwitchContent(int id, string message)
  {
    ID = id;
    Message = message;
  }

  public int ID { get; private set; }

  public string Message { get; private set; }
}

De message wordt een stuk eenvoudiger:

public class GenericSwitchMessage : GenericMessage<SwitchContent>
{
  public GenericSwitchMessage(SwitchContent switchParameter)
        : base(switchParameter)
  {
  }

  public override string ToString()
  {
    return String.Format(
      "Switch message: sender{0}; target={1}", Sender, Target);
  }
}

Het te verzenden bericht wordt nu gecombineerd met de context:

Messenger.Default.Send<GenericSwitchMessage>
  (new GenericSwitchMessage(new SwitchContent (1, "Hallo")));

En het uitluisten ziet er ook wat rustiger uit:

public class LogProvider : ILogProvider
{
  public void Initialize()
  {
    // log all messages
    Messenger.Default.Register<GenericSwitchMessage>(this, true, m =>
    {
      // m.Content.ID
      // m.Content.Message
    });
  }
}

Conclusie:

De MVVMLight Messenger biedt een fraaie oplossing voor de totale ontkoppeling. Er zijn ook andere implementaties beschikbaar (iedere MVVM implementatie lijkt er wel één te bieden) dus het is eenvoudig om er aan te komen en om het eens uit te proberen.

Een WPF StoryBoard starten vanuit het MVVM Viewmodel

 

In need of an English translation? Please drop me a note.

Sinds enige tijd werk ik aan een project waarbij de UI volledige in WPF4 is geschreven. Hierbij heb ik mezelf het doel gesteld om de architectuur volledig op MVVM te baseren.

Dit betekent concreet dat de schermen (de Views) geen code-behind hebben maar via een ViewModel interactie hebben met het Model. Alle schermwijzigingen worden met bindings en commands afgehandeld.

Zelf MVVM implementeren kan vast wel maar er zijn ook veel frameworks: grote, kleine, goede en foute… Uiteindelijk ben ik bij MVVMLight uitgekomen.

Ontwikkelen in WPF blijft door het gebruik van MVVM een feest, ondanks de steile leercurve maar dankzij de strikte scheiding tussen de te tonen data (ViewModel) en de representatie (XAML).

En ik ben ook begonnen met het gebruik van StoryBoards. Dit zijn notaties om de vorm verschijning en plaats van Xaml objecten aan te passen op een tijdslijn. Zo is het mogelijk om animaties in de UI te verwerken die wel degelijk van functioneel nut kunnen zijn.

StoryBoards worden bij voorkeur aan gebeurtenissen gehangen. Als voorbeeld toon ik hier hoe een plaatje langzaam zichtbaar wordt bij het starten van de applicatie door de Opacity in vijf seconden van 0 naar 1 te laten lopen.

Hieronder is de XAML beschreven en de code in het viewmodel. Dit zijn Snippets die zo te combineren zijn met een MvvmLight WPF4 template project.

<Grid x:Name="LayoutRoot"
      Width="284">
  <TextBox FontSize="36"
           FontWeight="Bold"
           Foreground="Purple"
           Text="{Binding WelcomeTitle}"
           VerticalAlignment="Center"
           HorizontalAlignment="Center"
           TextWrapping="Wrap"
           Margin="12,12,42,100" />
  <Image Height="51"
         Name="image"
         Source="Assets\troll-face.png"
         Opacity="0.5"
         Stretch="Uniform"
         HorizontalAlignment="Left"
         Margin="178,182,0,0"
         VerticalAlignment="Top"
         Width="61">
    <Image.Triggers>
      <EventTrigger RoutedEvent="Image.Loaded">
        <BeginStoryboard Name="MyBeginStoryboard">
          <Storyboard>
            <DoubleAnimation Storyboard.TargetProperty="Opacity"
                             From="0"
                             To="1"
                             Duration="0:0:5" />
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Image.Triggers>
  </Image>
</Grid>

Er staat een Textbox op de pagina voor de invoer van een string en een plaatje. Dit plaatje bevat een StoryBoard dit wordt afgevuurd als de pagina wordt geladen voor vertoning.

De Textbox is gebonden aan de WelcomeTitle op het ViewModel.

public class MainViewModel : ViewModelBase
{
  public MainViewModel()
  {
    WelcomeTitle = "Hello";
  }

  public const string WelcomeTitlePropertyName = "WelcomeTitle";
  private string _welcomeTitle = string.Empty;
  public string WelcomeTitle
  {
    get
    {
      return _welcomeTitle;
    }
    set
    {
      if (_welcomeTitle == value)
      {
        return;
      }

      _welcomeTitle = value;

      RaisePropertyChanged(WelcomeTitlePropertyName);
    }
  }
}

De uiteindelijke functionaliteit is eenvoudig. Indien de Textbox wordt verlaten zal de Getter van WelcomeTitle uitgevoerd worden.

Het scherm zier er zo uit:

Mijn wens is dat het Troll-plaatje pas getoond wordt als de textbox wordt verlaten en de WelcomeText korter dan drie karakters is.

Maar door de MVVM ‘belemmering’ (ik kan geen (visuele) XAML onderdelen direct vanuit code aanraken en aanspreken) wordt het lastig om StoryBoards aan te spreken. Een Command werkt ook niet want die vuurt juist in de richting van het viewmodel af (bv. indien de gebruiker een knop indruk of via de MVVMLight EventToCommand).

Ik moet iets anders gebruiken en het antwoord is: DataTriggers.

Het is mogelijk om de XAML naar wijzigingen op het ViewModel te laten luisteren en hierop te laten reageren.

Wat heb ik nodig?

  1. Een bindable (boolean) property op het ViewModel. Hiervoor voeg ik de “ShowTroll” property toe aan het ViewModel.
  2. Op het Viewmodel moet de nieuwe property van waarde kunnen veranderen. Dit gebeurt in de Setter van de WelcomeTitle. Als een waarde met een lengte kleiner dan drie karakters voorbij komt, dan wordt de ShowTroll op True gezet.
  3. Een Button in de Xaml. Deze doet op zich niets maar kan wel de Focus krijgen. Als deze aangeraakt wordt, zal de TextBox de focus verliezen en wordt de Setter van WelcomeTitle uitgevoerd. Een dummy dus.
  4. Een DataTrigger op de Image, gekoppeld aan de ShowTroll boolean
  5. Twee StoryBoards. Als de ShowTroll op true wordt gezet, zal de ene afgespeeld worden en anders de andere.

Dit betekent dus dat het viewmodel met een ShowTroll property wordt  uitgebreid en dat deze verandert in de Setter van de WelcomeTitle property:

public class MainViewModel : ViewModelBase
{
  public MainViewModel()
  {
    WelcomeTitle = "Hello";
  }

  public const string WelcomeTitlePropertyName = "WelcomeTitle";
  private string _welcomeTitle = string.Empty;
  public string WelcomeTitle
  {
    get
    {
      return _welcomeTitle;
    }
    set
    {
      if (_welcomeTitle == value)
      {
        return;
      }

      _welcomeTitle = value;
      ShowTroll = (_welcomeTitle.Length < 3); // Force data change
      RaisePropertyChanged(WelcomeTitlePropertyName);
    }
  }

  public const string ShowTrollPropertyName = "ShowTroll";
  private bool _ShowTroll = false;
  public bool ShowTroll
  {
    get
    {
      return _ShowTroll;
    }
    set
    {
      if (_ShowTroll == value)
      {
        return;
      }

      _ShowTroll = value;
      RaisePropertyChanged(ShowTrollPropertyName);
    }
  }
}

En er wordt dus in de Xaml een Button toegevoegd en de Image wordt iets aangepast. Zie dat de Opacity standaard nul (volledige doorzichtig) is. En er zijn twee StoryBoards om de Opacity binnen twee seconden op één of op nul te brengen.

De DataTrigger is aan de ShowTroll property gekoppeld en reageert zowel op True als op False.

<Grid x:Name="LayoutRoot"
      Width="284">
  <TextBox FontSize="36"
           FontWeight="Bold"
           Foreground="Purple"
           Text="{Binding WelcomeTitle}"
           VerticalAlignment="Center"
           HorizontalAlignment="Center"
           TextWrapping="Wrap"
           Margin="12,12,42,100" />
  <Button Content="Button"
          Height="23"
          HorizontalAlignment="Left"
          Margin="31,226,0,0"
          Name="button1"
          VerticalAlignment="Top"
          Width="75" />
  <Image Height="51"
         Name="image"
         Source="Assets\troll-face.png"
         Opacity="0"
         Stretch="Uniform"
         HorizontalAlignment="Left"
         Margin="178,182,0,0"
         VerticalAlignment="Top"
         Width="61">
    <Image.Resources>
      <Storyboard x:Key="TrollBeginStoryboard">
        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                         From="0"
                         To="1"
                         Duration="0:0:2" />
      </Storyboard>
      <Storyboard x:Key="TrollEndStoryboard">
        <DoubleAnimation Storyboard.TargetProperty="Opacity"
                         From="1"
                         To="0"
                         Duration="0:0:2" />
      </Storyboard>
    </Image.Resources>
    <Image.Style>
      <Style>
        <Style.Triggers>
          <DataTrigger Binding="{Binding ShowTroll}"
                       Value="true">
            <DataTrigger.EnterActions>
              <BeginStoryboard>
                <Storyboard>
                  <StaticResource ResourceKey="TrollBeginStoryboard" />
                </Storyboard>
              </BeginStoryboard>
            </DataTrigger.EnterActions>
            <DataTrigger.ExitActions>
              <BeginStoryboard>
                <Storyboard>
                  <StaticResource ResourceKey="TrollEndStoryboard" />
                </Storyboard>
              </BeginStoryboard>
            </DataTrigger.ExitActions>
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </Image.Style>
  </Image>
</Grid>

Hiervoor gebruiken we zowel de EnterActions als de ExitActions.

Dit geeft dus als effect dat als er een langere tekst is ingevoerd, de ‘troll’ niet zichtbaar is.

Maar als de gebruiker een tekst korter dan drie karakters invult, dan wordt binnen twee seconden de Troll zichtbaar.

En indien de tekst weer langer wordt gemaakt, dan is de Troll weer verstopt.

Ik wil in dit voorbeeld aangeven dat het relatief eenvoudig is om complexe UI veranderingen door te voeren door slecht een property op een ViewModel te manipuleren. Hiermee wordt het opeens mogelijk om binnen StoryBoards meerdere objecten te wijzigen. Dit is oogsnoep voor de gebruiker en zo wordt het een visueel feestje voor zowel de ontwikkelaar (of moet ik zeggen: Interactive Designer) en de gebruiker.

WPF en CamSpace, een leuke combinatie

Zo, de vakantie van 2008 is weer voorbij. In het Tatra gebergte is geen enkele
mogelijkheid voor een online verbinding en dat had best nog even mogen zo duren maar op een gegeven moment gaat het toch weer kriebelen.

De vakantieperiode is een uitgelezen moment om eens wat anders te onderzoeken dan wat je de rest van het bij de klant tegenkomt. De waan van de dag, zeg maar.
Zo kwam ik enige tijd geleden een demo tegen van een startup welke d.m.v.
beeldherkenning de gebruiker applicatie kon laten besturen. Dit zou met een
doodnormale webcam kunnen en daardoor kon ieder glimmend voorwerk een soort van Wii controller worden.

http://www.youtube.com/watch?v=v0srY37kkMw

Tijdens mijn vakantie kreeg ik de uitnodiging voor het beta traject dus de
afgelopen week ben ik er eens ingedoken.

Het is echt knap gemaakt, het werkt!

Met mijn Trust webcam van 14,95 en met vier fps (‘savonds bij kunstlicht)
blijkt het opeens mogelijk om een virtuele xylofoon te besturen met twee
markers. Wat ze doen is via een agent de webcam input omzetten naar een virtuele muis, joystick of toetsenbord. Het ondersteunt zelfs multi point input en er zijn veel variabelen in te stellen. Deze instellingen kunnen dan aan een
executable gehangen worden waardoor tijdens het opstarten van die executable, de input doorgegeven wordt.

Het was even zoeken maar er zat ook een SDK bij. Hiermee kunnen ontwikkelaars de huidige telemetrie uitlezen en de CamSpace host applicatie enigzins manipuleren. Helaas is er geen handleiding en ook de wiki is vooral leeg, maar het is redelijk zelfbeschrijvend. Helaas werkt het ontvangen van de input niet via callbackfunctionaliteit, ik moet hiervoor met een Timer werken. Aan de andere kant, we krijgen maar maximaal zo’n dertig keer per seconde gegevens door (ik dus maar vier keer onder slechte omstandigheiden) dus die timer werkt prima.

En zoals gezegd, het werkt als een fluitje en de waarden komen meestal door
als relatieve waarden voor hwet gemak. Zo geeft CamSpace door dat b.v. de X
waarde van een coordinaat op een derde van de breedte van het scherm staat.

Ik heb er eerst een met een WinForm applicatie tegenaan geprogrammeerd, maar helaas, daar zijn buttons niet te draaien. Dit gaf mij dus eindelijk een reden
om eens echt met WPF aan de gang te gaan. Ik had het in het verleden al wel eens vluchtig bekeken maar dit is dan toch weer in het diepe stappen.

Ik maakte eerst de fout om de verkeerde Timer te kiezen (system.Timers.Timer)
want die draait los van de UI thread. Dus zelfs het uitlezen van this.Width
werkte niet. Pas na de keuze voor de System.Windows.Threading.DispatcherTimer
begon het lopen en bleek ook de binding toch nog mee te vallen.

Zie hier het huidige resultaat van een paar avondjes knutselen:

Een beetje subtiel, maar CamSpace geeft ook de breedte door (dichtbij/veraf)
en die kan mooi in een smallere of bredere knop vertaald worden.

Wat nog fraaier was, bleek bij het uitzoeken van de rotatie. Dit was even
zoeken, maar die werden in radialen doorgegeven. En het mooie is dat CamSpace echt onthoudt hoeveel keer het ‘input device’ geroteerd is. Na vijf slagen linksom, kan je ook weer aftellen naar vijf slagen rechtsom. De simulatie van een schroevendraaier is daarbij dus een stuk dichterbij gekomen 😉

En hiermee komen we dus direct bij de conclusie: leuk stukje speelgoed, maar
wat kan je er mee. Datzelfde was ook de reactie op de eerste berichten over de
Wii controller…