Posts mit dem Label wpf werden angezeigt. Alle Posts anzeigen
Posts mit dem Label wpf werden angezeigt. Alle Posts anzeigen

Donnerstag, 18. Juli 2013

Code Snippet: ViewModelBase

Oft benötige ich eine Implementierung der ViewModelBase-Klasse, dann muss ich diese entweder aus anderen Projekten kopieren, oder googeln. Jetzt muss ich das nicht mehr, denn ich merk sie mir einfach hier:

// parts of this code is based upon the article : http://msdn.microsoft.com/de-de/magazine/dd419663.aspx#id0090051
class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// raises the PropertyChanged event with the given string parameter
    /// </summary>
    /// <param name="propertyName">name of the property, that has changed</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        // just to avoid wrong Event-Calls (only in debug mode)
        this.VerifyPropertyName(propertyName);

        // call the event handler
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }

    /// <summary>
    /// raises the PropertyChanged-event, with the name of the property that is given by the lambda expression
    /// </summary>
    /// <typeparam name="Tprop">type parameter of the given property, to be ignored</typeparam>
    /// <param name="property">the lambda expression f.e.: "() => TestProperty"</param>
    protected virtual void OnPropertyChanged<Tprop>(Expression<Func<Tprop>> property)
    {
        // cast as LambdaExpression
        LambdaExpression lambda_exp = (LambdaExpression)property;
        if (lambda_exp != null) 
        {
            MemberExpression member_exp = (MemberExpression)lambda_exp.Body;
            if (member_exp != null)
            {
                OnPropertyChanged(member_exp.Member.Name);
                return;
            }
        }
        Debug.Fail("No correct LambdaExpression provided for OnPropertyChanged.");
    }

    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyPropertyName(string propertyName)
    {
        // Verify that the property name matches a real,  
        // public, instance property on this object.
        if (TypeDescriptor.GetProperties(this)[propertyName] == null)
        {
            string msg = "Invalid property name: " + propertyName;
            Debug.Fail(msg);
        }
    }
}

Montag, 8. Oktober 2012

WPF Diagramm zum Anzeigen der CPU Auslastung

Herausforderung

Ziel heute ist es ein eigenständiges UserControl in WPF zu schreiben welches "as is" in jede beliebige Alikation eingebunden werden. Aufgabe des UserControls ist das Anzeigen der Prozessorauslastung als Liniendiagramm.

1. Wie messe ich die aktuelle CPU Auslastung in %

Möglich wird das über den PerformanceCounter aus der Assembly System.Diagnostics
PerformanceCounter cpuCounter = new PerformanceCounter();
cpuCounter.CategoryName = "Processor";
cpuCounter.CounterName = "% Processor Time";
cpuCounter.InstanceName = "_Total";

Console.WriteLine("CPU Auslastung: {0} %", cpuCounter.NextValue());

Das war ja schonmal einfach... Jetzt fehlt nurnoch das Diagramm.

2. Liniendiagramm im XAML

Wie der Name schon sagt, besteht ein Liniendiagramm aus Linien, d.h wir benötigen ein ItemsControl, dass an eine Liste von Start-End-Punkten gebunden wird.
<ItemsControl ItemsSource="{Binding CPUUsagePoints}">
 <ItemsControl.ItemTemplate>
  <DataTemplate>
   <Canvas>
    <Line 
          X1="{Binding Start.X}" Y1="{Binding Start.Y}" 
          X2="{Binding End.X}" Y2="{Binding End.Y}" 
          Stroke="Green" StrokeThickness="0.3"
          >
    </Line>
   </Canvas>
  </DataTemplate>
 </ItemsControl.ItemTemplate>
</ItemsControl>
Da der C# System.Drawing.Point kein Referenzdatentyp, sondern ein Verbunddatentyp ist, benötigen wir eine analoge Klasse, und dazu noch eine Klasse für eine Strecke mit Start-Punkt und End-Punkt. Die Implementierung spar ich mir hier, und zeige lieber das aus dem ViewModel gebundene Property CPUUsagePoints.
public List<Point> _pointList = new List<Point>();
public List<Line> _diagramLines;
/// <summary>
/// diagram lines for binding
/// </summary>
public List<Line> DiagramLines
{
  get
  {
    if (_diagramLines == null)
    {
     _diagramLines = new List<Line>();
     InitDiagramm();
    }
  return _diagramLines;
  }
}

/// <summary>
/// initializes the diagram with f(x)=0
/// and connects the points with lines
/// </summary>
void InitDiagramm()
{
  int granularity = 2;
  for (int i = 0; i < (int)(_parent.RenderSize.Width / granularity); i++)
  {
    Line sep = new Line();
    if (_diagramLines.Count == 0)
    {
      sep.Start = new Point(i * granularity, _parent.RenderSize.Height - 1);
      _pointList.Add(sep.Start);
    }
    else
    {
      sep.Start = _diagramLines.Last().End;
    }

    sep.End = new Point(i * granularity, _parent.RenderSize.Height - 1);
    _pointList.Add(sep.End);

    _diagramLines.Add(sep);
  }
}

Wie man leicht sieht werden die Punkte so initialisiert, dass diese bei einem kartesischen Koordinatensystem die Funktion f(x)=0 abbilden, also eine klassische Nulllinie. Da wir in der Computergrafik ein anderes Koordinatensystem verwenden, müssen wir das Ganze die Y-Achse betreffend noch etwas umrechnen.

So weit, so gut - aber wie sollen wir nun die neuen Werte in das Diagramm "rein" bekommen, dafür gibt es bestimmt einige Möglichkeiten - ich habe mich dafür entschieden, die neuen Werte rechts anzuhängen und die bestehenden Punkte nach links zu verschieben. Abstrakt gesehen, brauche ich hier also 2 Funktionen:
  1. Füge_neuen_Wert_hinzu (prozent)
  2. Schiebe_alle_bisherigen_Punkte_nach_links()
Nicht ganz so abstrakt, aber mindestens genauso einfach wird die Implementierung:
/// <summary>
/// add a new value for the cpu usage
/// </summary>
/// <param name="percent">value in per cent</param>
void AddCpuUsage(int percent)
{
  try
  {
    percent = 100 - percent;
    lock (_pointList)
    {
      ShiftLeft(1);
      Point p = _pointList[_pointList.Count - 1];
      double valueToAchieve = ((float)percent / 100.0) * _parent.RenderSize.Height;
      p.Y = valueToAchieve;
    }
  }
  catch { }
 }

/// <summary>
/// shifts the diagram points left
/// </summary>
/// <param name="offset">the offset, how far to shift</param>
void ShiftLeft(int offset = 1)
{
  for (int i = 0; i < _pointList.Count - offset; i++)
  {
    Point p = _pointList[i];
    Point nextPoint = _pointList[i + offset];
    p.Y = nextPoint.Y;
  }
}
Wie man sieht, habe ich hier etwas geschummelt, ich verschiebe nicht die Punkte und hänge auch nichts hinten an, sondern shifte nur die Y-Werte nach links, und verändere dann den Y-Wert des letzten Punkts. Dadurch habe ich einen entscheidenden Vorteil - ich muss zum einen keine X-Verschiebung der einzelnen Punkte durchführen und zum Anderen kann ich die Liste DiagramLines so wie sie ist beibehalten, das Binding wird automatisch durch ein OnPropertyChanged in der Klasse Point bewerkstelligt.

Und für alle, die das Ganze nochmal praktisch nachvollziehen wollen, hier der Link zur Solution:
Solution CPUUsageVisualizer

Freitag, 28. September 2012

Code Snippet: Thread sicherer Zugriff auf WPF-Control

Motivation

Verwendet man in WPF konsequent das MVVM-Modell, so greift man immer auf das ViewModel zu, anstatt direkt mit irgendwelchen GUI-Elementen zu interagieren. Da der Zugriff auf das ViewModel wiederum threadunabhängig ist, kommt man eigentlich nie (oder eher selten) in die Lage folgenden Code einzusetzen. Aber wie immer bestätigen Ausnahmen die Regel - mir fällt zwar spontan kein einfacher Anwendungsfall ein, aber in der täglichen Praxis habe ich diesen Code schon einige male verwendet:

// nehmen wir an, wir haben eine TextBox und wollen threadsicher den Text verändern
 class ThreadSafeTextBox : TextBox
    {
        public void SetTextThreadSafe(string text)
        {
                // prüft, ob der aktuelle Thread dem Dispatcher 
                // dieses Controls zugeordnet ist
                if (this.Dispatcher.CheckAccess())
                {
                    // Ist dies der Fall, können wir den Text einfach setzen.
                    // Man sollte beachten, dass man sich jetzt in dem 
                    // Dispatcher Thread befindet, man sollte hier also nur 
                    // "schnell" auf die GUI zugreifen. Alles was lange dauert
                    // und nichts direkt mit der GUI zu un hat, gehört hier nicht rein!
                    base.Text = text;
                }
                else
                {
                    // anderenfalls müssen wir den Dispatcher "invoken"
                    this.Dispatcher.Invoke(new Action(
                        () =>
                        {
                            // jetzt können wir sicher auf den Text zugreifen
                            SetTextThreadSafe(text);
                        })
                        );
                }
            }
        }
    }

Donnerstag, 27. September 2012

Code Snippet: XAML Progress Indicator (Marquee)

Motivation

In Windows 95 haben wir gemerkt, dass Fortschrittsbalken nicht nur wachsen, sondern auch schrumpfen können. Progressive Fortschrittskorrektur nennt das der Klugscheißer, sogar die Überschreitung der 100% war in der Vergangenheit durchaus möglich.

Für den Anwender sind Fortschrittsbalken hauptsächlich ein Indikator dafür, dass ein Vorgang noch sehr lange dauert - Wenn der Balken nicht zuverlässig ist, sollte man ihn ganz lassen, oder eben nur durch "Bewegung" dem Anwender signalisieren, dass etwas bearbeitet wird. Bei diversen Videoplattformen kennt man ddies als "den Kreisel des Wartens". In Windows 8 jedoch sieht man oft eine Schlange von Punkten die von links nach rechts Pendeln, diese habe ich hier in XAML nachempfunden, viel Spaß damit:
 

<Canvas VerticalAlignment="Center" HorizontalAlignment="Center">
 <Path Fill="Black" Name="Path5">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell5"></EllipseGeometry>
  </Path.Data>
 </Path>

 <Path Fill="Black" Name="Path4">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell4"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path3">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell3"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path2">
  <Path.Data >
   <EllipseGeometry Center="15,0" RadiusX="3.8" RadiusY="3.8" x:Name="Ell2"></EllipseGeometry>
  </Path.Data>
 </Path>
 <Path Fill="Black" Name="Path1">
  <Path.Data >
   <GeometryGroup>
    <EllipseGeometry Center="15,0" RadiusX="3.5" RadiusY="3.5" x:Name="Ell1"></EllipseGeometry>
   </GeometryGroup>

  </Path.Data>
  <Path.Triggers>
   <EventTrigger RoutedEvent="Path.Loaded">
    <BeginStoryboard>
     <Storyboard SpeedRatio="4">
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell1" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:2" Value="32,0" />
       <EasingPointKeyFrame KeyTime="0:0:8" Value="42,0" />
       <EasingPointKeyFrame KeyTime="0:0:10" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell2" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:1" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:3" Value="15,0" />
       <EasingPointKeyFrame KeyTime="0:0:9" Value="25,0" />
       <EasingPointKeyFrame KeyTime="0:0:11" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell3" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:2" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:4" Value="0,0" />
       <EasingPointKeyFrame KeyTime="0:0:10" Value="10,0" />
       <EasingPointKeyFrame KeyTime="0:0:12" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell4" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:3" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:5" Value="-15,0" />
       <EasingPointKeyFrame KeyTime="0:0:11" Value="-5,0" />
       <EasingPointKeyFrame KeyTime="0:0:13" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>
      <PointAnimationUsingKeyFrames
        Storyboard.TargetName="Ell5" Storyboard.TargetProperty="Center"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever"
                >
       <EasingPointKeyFrame KeyTime="0:0:0" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:4" Value="-100,0" />
       <EasingPointKeyFrame KeyTime="0:0:6" Value="-30,0" />
       <EasingPointKeyFrame KeyTime="0:0:12" Value="-20,0" />
       <EasingPointKeyFrame KeyTime="0:0:14" Value="100,0" />
       <EasingPointKeyFrame KeyTime="0:0:15" Value="100,0" />
      </PointAnimationUsingKeyFrames>

      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path1" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:2" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:8" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:10" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path2" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:1" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:3" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:9" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:11" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path3" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:2" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:4" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:10" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:12" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path4" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:3" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:5" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:11" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:13" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
      <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Path5" Storyboard.TargetProperty="Opacity"
        Duration="0:0:15" FillBehavior="Stop" RepeatBehavior="Forever">
       <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:4" Value="0"/>
       <EasingDoubleKeyFrame KeyTime="0:0:6" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:12" Value="1"/>
       <EasingDoubleKeyFrame KeyTime="0:0:14" Value="0"/>
      </DoubleAnimationUsingKeyFrames>
     </Storyboard>
    </BeginStoryboard>
   </EventTrigger>
  </Path.Triggers>
 </Path>
</Canvas>