Blazor Lab Ampel

Vor vielen Jahren habe ich hier einen Blog Artikel geschrieben, wie man eine Österreichische Ampel mit Silverlight realisiert. Der Artikel ist noch online, aber die Bilder sind leider verloren gegangen, so dass ich auf eine Verlinkung verzichte.

Ja nun ist da Blazor das neue hippe Kid in Town und so habe ich mir vorgenommen das gleiche Problem mit Blazor zu lösen. Eines vorweg, Silverlight ist einfacher, hat einen besseren Designer, trennt die Verantwortungen besser, braucht weniger Code und man ist schneller fertig. Blazor ist HTML, CSS und C#..

Dieses Blazor Übungs Lab Tutorial zeigt wie man eine Ampel mit dem typischen grün blinken programmiert.

Ich gehe von einem neuen Visual Studio Blazor Projekt aus. Dabei spielt es keine Rolle ob Webassembly oder Server Side. 

Der Zustand/State

Funktionell definiert sich eine Ampel über Zustände. Diese werden gesetzt und bewirken eine Zustandsänderung. Am Ende soll die Ampel über einen Timer gesteuert, zwischen den Lampen hin und her schalten, wie man das vom Verkehr so gewöhnt ist. In XAML geht das ganz leicht mit dem VisualStatemanager. Hier wird erstmal eine Enum in einer Datei VisualState.cs definiert. Bei mir im Verzeichnis Data

   1:    public enum VisualState
   2:    {
   3:          Off,
   4:          Stop,
   5:          SwitchGreen,
   6:          SwitchRed,
   7:          Go,
   8:          GoEnd
   9:    }

 

Die Glühbirne

Fügen sie in das Verzeichnis Pages eine neue Blazor Page Lampe.Razor hinzu. Diese dient als eine Art UserControl. Das UI wird aus HTML und CSS zusammengesetzt. Die Blinkende Animation kommt auch wieder per CSS und die Logik wie sich die Lampe je nach Status verhalten soll aus C# Code.

Im HTML Teil fügen sie ein

   1:  @using Ampel.Data
   2:  <div class="circle @Farbeintern"></div>
   3:   
   4:  @code {
   5:      [Parameter]
   6:      public string Farbe { get; set; }
   7:      [Parameter]
   8:      public Ampel.Data.VisualState DisplayState { get; set; }
   9:      public string Farbeintern { get; set; }

Damit das DIV ein runder Kreis wird benötigen sie eine CSS Datei, die ident zur Blazor Datei benannt wird als ampel.razors.css. Dies wird als CSS Isolation bezeichnet.

Erzeugen sie die CSS Klassen für den Kreis, die Farbschemas und für die Grün Blink Animation.

   1:  .circle {
   2:      background-color: rgba(0, 0, 0, 0.3);
   3:      border-radius: 100%;
   4:      position: relative;
   5:      height: 120px;
   6:      width: 120px;
   7:      margin: 20px;
   8:  }
   9:   
  10:  .black {
  11:      background-color: black;
  12:      box-shadow: 0 0;
  13:  }
  14:   
  15:  .red {
  16:      background-color: #c0392b;
  17:      box-shadow: 0 0 10px 5px #c0392b;
  18:  }
  19:   
  20:  .yellow {
  21:      background-color: #ffd800;
  22:      box-shadow: 0 0 10px 5px #ffd800;
  23:  }
  24:   
  25:  .green {
  26:      background-color: #7ce857;
  27:      box-shadow: 0 0 10px 5px #7ce857;
  28:  }
  29:   
  30:  .blink {
  31:      animation-name: blink;
  32:      animation-duration: 1s;
  33:      animation-iteration-count: 10;
  34:      animation-direction: alternate;
  35:  }
  36:   
  37:   
  38:  @keyframes blink {
  39:      from {
  40:          background-color: #7ce857;
  41:          box-shadow: 0 0 10px 5px #7ce857;
  42:      }
  43:   
  44:      to {
  45:          background-color: black;
  46:          box-shadow: 0 0 ;
  47:      }
  48:  }

 

Die Ampel

Nun können Sie die Ampellichter mehrfach in einer weiteren Blazor Page verwenden. Ändern Sie nun die Datei index.razor so das nur noch folgender Code enthalten ist.

   1:  @page "/"
   2:  @using Ampel.Data
   3:  <h1>Ampel</h1>
   4:  <div class="ampel">
   5:      <Lampe Farbe="red" DisplayState="@(red)"></Lampe>
   6:      <Lampe Farbe="yellow" DisplayState="@(yellow)"></Lampe>
   7:      <Lampe Farbe="green" DisplayState="@(green)"></Lampe>
   8:  </div>
   9:  @code
  10:  {
  11:      public VisualState ActualState { get; set; }
  12:      public VisualState red { get; set; }
  13:      public VisualState yellow { get; set; }
  14:      public VisualState green { get; set; }

Das Using Statement muss auf den App Pfad verweisen wo ihre VisualState Enum gespeichert ist. Die Farbe der Lampe definiert sich über das gleichnamige Property, per Parameter Annotation. Da die Lampe mehrer Zustände (An/Aus/Blinkend) haben kann, wird ein weiteres Property DisplayState benötigt, das an die Eigenschaft red/yellow/green gebunden ist. Die Ampel hat einen gesamten State der den aktuellen Zustand wiederspiegelt.

Starten sie nun die Anwendung. Das Ergebnis ist noch unspektakulär

image

Über eine CSS Datei wird nun der Ampel Rahmen gestylt. Fügen Sie im Pages Verzeichnis die Datei Index.razor.css hinzu.

   1:  .ampel {
   2:      background: #222;
   3:      width: 170px;
   4:      height: 440px;
   5:      border-radius: 20px;
   6:      position: relative;
   7:      border: solid 5px #333;
   8:  }

image

Statuswechsel Ampel Logik

In der Blazor Page wird ein Button mit gebundenen Event eingefügt um den State manuell wechseln zu können. Dazu lassen wir uns den Status auch anzeigen, da die Lampen noch keine Logik beinhalten.

   1:  <button @onclick="statuswechsel">ding</button>
   2:  @ActualState

 

Fügen Sie innerhalb des @code eine Function Statuswechsel ein. Lassen sie durch tippen von switch TAB TAB den Rumpf erstellen. Per Tab wird der Bereich switch_on markiert

image

Ändern sie in ActualState (die Enum)

image

Visual Studio ergänzt nun automatisch für alle vorhanden Enum Werte die Case Zweige.

Kompletieren Sie den Code um die Logik, die den Status konkret den Lampen zuweist.

   1:  public void statuswechsel()
   2:  {
   3:    switch (ActualState)
   4:    {
   5:   case VisualState.Off:
   6:      ActualState = VisualState.Stop;
   7:      red = VisualState.Stop;
   8:      yellow = VisualState.Off;
   9:      green = VisualState.Off;
  10:      break;
  11:  case VisualState.Stop:
  12:      ActualState = VisualState.SwitchGreen;
  13:      red = VisualState.Off;
  14:      yellow = VisualState.SwitchGreen;
  15:      green = VisualState.Off;
  16:      break;
  17:  case VisualState.SwitchGreen:
  18:      ActualState = VisualState.Go;
  19:      red = VisualState.Off;
  20:      yellow = VisualState.Off;
  21:      green = VisualState.Go;
  22:      break;
  23:  case VisualState.SwitchRed:
  24:      ActualState = VisualState.Stop;
  25:      red = VisualState.Stop;
  26:      yellow = VisualState.Off;
  27:      green = VisualState.Off;
  28:      break;
  29:   case VisualState.Go:
  30:      ActualState = VisualState.GoEnd;
  31:      red = VisualState.Off;
  32:      yellow = VisualState.Off;
  33:      green = VisualState.GoEnd;
  34:      break;
  35:   case VisualState.GoEnd:
  36:      ActualState = VisualState.SwitchRed;
  37:      red = VisualState.Off;
  38:      yellow = VisualState.SwitchRed;
  39:      green = VisualState.Off;
  40:      break;
  41:  default:
  42:      break;
  43:   }

Starten Sie nun die Ampel BlazorApp um die Funktion des Buttons zu prüfen.

Status der Lampe Logik

Wechseln Sie in die Ampel.razor Komponente um den Code einzufügen, der die Umschaltung der CSS Klassen auslöst. Die Lampe hat nun das visuelle Verhalten implementiert.

   1:   protected override void OnParametersSet()
   2:   {
   3:    StateChange(DisplayState);
   4:  }
   5:  void StateChange(VisualState Animation)
   6:  {
   7:   switch (Animation)
   8:   {
   9:   case VisualState.Off:
  10:    Farbeintern = "black";
  11:    break;
  12:  case VisualState.Go:
  13:    if (Farbe == "green")
  14:    {
  15:      Farbeintern = "green";
  16:    }
  17:    break;
  18:  case VisualState.GoEnd:
  19:    if (Farbe == "green")
  20:    {
  21:     Farbeintern = "green blink";
  22:    }
  23:    break;
  24:   case VisualState.SwitchRed:
  25:    if (Farbe == "yellow")
  26:    {
  27:     Farbeintern = "yellow";
  28:     }
  29:    break;
  30:    case VisualState.SwitchGreen:
  31:    if (Farbe == "yellow")
  32:    {
  33:      Farbeintern = "yellow";
  34:    }
  35:    break;
  36:   case VisualState.Stop:
  37:    if (Farbe == "red")
  38:    {
  39:    Farbeintern = "red";
  40:    }
  41:    break;
  42:   default:
  43:    break;
  44:  }

Damit lässt sich die manuelle Ampel nun testen

ampel 

Automatisch mit Timer

Im nächsten Schritt von Handsteuerung auf Automatik umgestellt. Dafür wird ein .NET Timer benutzt. Diesen wird völlig neu gestaltet. Der Übergang von Phase zu Phase hat verschiedene variable Zeitfenster. Erstellen sie eine neue Klasse TimerService.cs im Data Verzeichnis. Im Konstruktor (ctor TAB TAB) wird der Timer erstellt, das Timing Event zugewiesen und die einmalige Ausführung fest gelegt.

Diese Klasse bekommt ein eigene Event OnEleapsed das vom Timer ausgelöst (Invoke) wird.

   1:  public TimerService()
   2:   {
   3:              _timer = new Timer();
   4:              _timer.Elapsed += NotifyTimerElapsed;
   5:              _timer.Enabled = true;
   6:              _timer.AutoReset = false;
   7:          }
   8:          private Timer _timer;
   9:   
  10: public void SetTimer(double interval)
  11:          {
  12:              _timer.Interval = interval;
  13:              _timer.Start();
  14:          }
  15:   
  16:          public event Action OnElapsed;
  17:   
  18: private void NotifyTimerElapsed(Object source, ElapsedEventArgs e)
  19:          {
  20:              OnElapsed?.Invoke();
  21:          
  22:          }

Aufgrund dieser speziellen Ausführung kann nun die Klasse im Dependency Container der Blazor App registriert werden. Das geschieht in der Datei Startup.cs

   1:  public void ConfigureServices(IServiceCollection services)
   2:   {....
   3:      services.AddSingleton<TimerService>();
   4:    }

Wechseln Sie nun in die Index.razor um den Timer zu injizieren

   1:  @page "/"
   2:  @using Ampel.Data
   3:  @inject TimerService Timer

 

Es gibt nun drei relevante Dinge zu tun. Statt dem Button Event wird nun der Timer mit dem Statuswechsel Event verknüpft. Einmalig im Onitialized Lifecycle Event. Beim Statuswechsel wird der Timer mit einer neuen individuellen Zeitspanne (in Millisekunden) neu gestartet.

Da der Timer asynchron ausgeführt wird, muss der HTML Rendervorgang manuell ausgelöst werden. Dazu per InvokeAsync speziell im UI Thread.

   1:   protected override void OnInitialized()
   2:      {
   3:          Timer.OnElapsed += statuswechsel;
   4:      }
   5:      public void statuswechsel()
   6:      {
   7:          switch (ActualState)
   8:          {
   9:              case VisualState.Off:
  10:                  ActualState = VisualState.Stop;
  11:                  red = VisualState.Stop;
  12:                  yellow = VisualState.Off;
  13:                  green = VisualState.Off;
  14:                  Timer.SetTimer(3000);
  15:             
  16:                  break;
  17:              case VisualState.Stop:
  18:                  ActualState = VisualState.SwitchGreen;
  19:                  red = VisualState.Off;
  20:                  yellow = VisualState.SwitchGreen;
  21:                  green = VisualState.Off;
  22:                  Timer.SetTimer(1000);
  23:                  break;
  24:              case VisualState.SwitchGreen:
  25:                  ActualState = VisualState.Go;
  26:                  red = VisualState.Off;
  27:                  yellow = VisualState.Off;
  28:                  green = VisualState.Go;
  29:                  Timer.SetTimer(3000);
  30:                  break;
  31:              case VisualState.SwitchRed:
  32:                  ActualState = VisualState.Stop;
  33:                  red = VisualState.Stop;
  34:                  yellow = VisualState.Off;
  35:                  green = VisualState.Off;
  36:                  Timer.SetTimer(3000);
  37:                  break;
  38:              case VisualState.Go:
  39:                  ActualState = VisualState.GoEnd;
  40:                  red = VisualState.Off;
  41:                  yellow = VisualState.Off;
  42:                  green = VisualState.GoEnd;
  43:                  Timer.SetTimer(3000);
  44:                  break;
  45:              case VisualState.GoEnd:
  46:                  ActualState = VisualState.SwitchRed;
  47:                  red = VisualState.Off;
  48:                  yellow = VisualState.SwitchRed;
  49:                  green = VisualState.Off;
  50:                  Timer.SetTimer(1000);
  51:                  break;
  52:              default:
  53:                  break;
  54:          }
  55:          InvokeAsync(StateHasChanged);
  56:      }

 

Ihre Ampel läuft nun nach Start der Blazor App automatisch durch

Kommentare sind geschlossen