The Architect
aka "DotNetMastermind"

Wissenswertes zur Entwicklung hochwertiger grafischer Oberflächen (Rich User Interfaces) in WPF, Silverlight (dotNet, .NET) und Silverlight for Windows Phone 7 (WP7)
Twitter Client UI mit Zeichenzähler und automatischer URL Kürzung

Als langjähriger und treuer dotnetpro Leser, wurde ich in der aktuellen Ausgabe 11.2013 mit einer sehr interessanten Aufgabe konfrontiert. In den letzten zwei dotnetpro Ausgaben hat der Autor Stefan Lieser eine Twitter Anwendung implementiert, mit welcher es möglich ist, Tweets verzögert an den Nachrichtendienst Twitter zu übermitteln. Als letzter Schritt sollte nun noch die Benutzerschnittstelle überarbeitet werden. Aufgrund meiner GUI Affinität und WPF Begeisterung fand ich die Aufgabe derart interessant, dass ich hier meinen Lösungsweg darstellen möchte.

Gegeben war also eine simple Benutzerschnittstelle zum Versenden von Nachrichten an den Cloud Service Twitter. Die Aufgabe war es nun, folgende Features zu der bereits vorhandenen Benutzerschnittstelle hinzuzufügen :

1. Alle URLs in dem eingegebenen Text sollten nach einer bestimmten Zeitspanne automatisch gekürzt werden. Optional sollte die URLs zunächst in Hyperlinks umgewandelt und erst nach einem Klick darauf gekürzt werden.

2. Anzeige der noch verbleibenden Zeichen (unter Berücksichtigung bereits gekürzter URLs).

3. Auswahl des Veröffentlichungszeitpunktes mittels eines DateTimePicker Steuerelementes.

Meine persönliche Vorgabe war darüber hinaus : “Die Implementierung der Lösung sollte unter Beachtung der Clean Code Prinzipien erfolgen und es sollte eine maximale Wiederverwendbarkeit der einzelnen Komponenten gegeben sein.”

Lösung der DOJO Aufgaben :

1. Das Kürzen der URLs wurde mittels eines Behavior‘s gelöst. Dadurch ist es möglich eine TextBox, mit einem Verhalten auszustatten, welches die in der Text-Eigenschaft enthaltenen URLs automatisch kürzt. Somit ist es völlig ausreichend, eine beliebige TextBox mit diesem Verhalten auszustatten und das Kürzen der darin enthaltenen URLs geschieht vollautomatisch. Darüber hinaus wurde das Behavior derart implementiert (Behavior für Objekte vom Typ TextBoxBase), dass dieses Behavior auch einer RichTextBox zugewiesen werden kann, um eine automatische Erzeugung von Hyperlink‘s zu bewirken und das Kürzen mittels eines Klicks auf diese Hyperlink‘s zu ermöglichen.

Vorteil : In der UI müssen lediglich wenige Zeilen XAML Code eingefügt werden, statt vieler (nicht wiederverwendbarer) Code Behind Zeilen. Das Behavior ist somit sehr leicht wiederverwendbar und erweiterbar (Komponentenorientierung).

2. Die Anzeige der noch verbleibenden Zeichen erfolgt mittels eines Value Converters. Darüber hinaus wurde auch ein weiterer Value Converter implementiert, welcher beim Überschreiten eines vorhandenen Zeichenlimits eine entsprechende Farbe zurückgibt. Zusätzlich wurden beide Konverter von der MarkupExtension Klasse abgeleitet. Somit ist es nicht nötig, eine Ressource für die Verwendung des Konverters im XAML Code anzulegen, sondern kann den Konverter mittels der Markuperweiterung-Syntax direkt verwenden.

Vorteil : In der UI müssen lediglich wenige Zeilen XAML Code (mittels Binding und Converter) eingefügt werden, statt vieler (nicht wiederverwendbarer) Code Behind Zeilen. Die Value Converter sind sehr leicht wiederverwendbar und erweiterbar (Komponentenorientierung).

3.   Für die Auswahl des Veröffentlichungszeitpunktes wurde nach einem passenden Control in den Tiefen des World Wide Web gesucht, da ein entsprechendes Standard Control garantiert bereits von jemand anderem implementiert wurde und man sich die Zeit für eine neue Implementierung sparen kann. Der DateTimePicker der AvalonControlLibrary stellt für diese Erfordernisse das geeignete Control bereit. Nachdem einige kleine Änderungen am Quellcode der AvalonControlLibrary vorgenommen wurden, war das Control einsatzbereit.

Vorteil : Da ein DateTimePicker Control bereits unzählige Male implementiert wurde, wäre es eine reine Zeitverschwendung gewesen, dieses erneut zu implementieren. Stattdessen habe ich den Code einer bereits bestehenden Klassenbibliothek geringfügig angepasst und wiederverwendet (Komponentenorientierung).

 

Beschreibung aller verwendeten Klassen :


StringToCharCountConverter

Kurzbeschreibung

Diese Klasse dient dazu, eine Zeichenkette in einen Integer-Wert zu konvertieren, welcher die Anzahl der Zeichen in der Zeichenkette enthält (ohne Sonderzeichen). Bei der Verwendung der MaxCount Eigenschaft enthält der Integer-Wert die Anzahl der noch verbleibenden Zeichen.

XAML Namespace

xmlns:conv="clr-namespace:SoftArcs.ValueConverter;assembly=SoftArcs.ValueConverter" 

Verwendung der Klasse

<TextBlock
           Text="{Binding ElementName=txtTweet, Path=Text,
                          Converter={conv:StringToCharCountConverter MaxCount=140}}"
 />

StringToCharCountBrushConverter

Kurzbeschreibung

Diese Klasse dient dazu, in Abhängigkeit der Länge einer Zeichenkette eine entsprechenden Farbe zurückzugeben. Solange MaxCount nicht überschritten ist, wird standardmäßig ein transparenter SolidSolorBrush zurückgegeben und sobald MaxCount überschritten ist, wird standardmäßig die Farbe Crimson (dunkles Rot) zurückgegeben. Die jeweiligen Farben können über die Eigenschaften DefaultBrush und MaxCountExceededBrush angepasst werden.

XAML Namespace

xmlns:conv="clr-namespace:SoftArcs.ValueConverter;assembly=SoftArcs.ValueConverter" 

Verwendung der Klasse

<TextBlock
           Text="{Binding ElementName=txtTweet, Path=Text,
                      Converter={conv:StringToCharCountBrushConverter MaxCount=140}}"
 />


UrlShorteningBehavior

Kurzbeschreibung

Mit dieser Klasse ist es möglich, eine TextBox bzw. eine RichTextBox mit einem Verhalten auszustatten, welches die im Text enthaltenen URLs automatisch kürzt. Das zeitverzögerte Kürzen der URLs kann mittels der Eigenschaft AutoShorteningDelayTime eingestellt werden (in Millisekunden). Bei einer RichTextBox wird eine URL zunächst in ein Hyperlink-Element umgewandelt und erst bei einem Klick auf diesen Hyperlink wird die URL gekürzt. Über die Eigenschaft AutoShorten kann das Kürzen auch ohne den Umweg über die Erzeugung eines Hyperlinks erfolgen.

XAML Namespaces

xmlns:conv="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:conv="clr-namespace:SoftArcs.UrlServices.UI.Behaviors;assembly=SoftArcs.UrlServices"

Verwendung der Klasse

<TextBox/>
        <i:Interaction.Behaviors>
                 <b:UrlShorteningBehavior AutoShorteningDelayTime="3000"/>
        </i:Interaction.Behaviors>
</TextBox> 


Verwendete Klassen

ShorteningService

RichTextBoxExtensions


ShorteningService

Kurzbeschreibung

Mit dieser Klasse ist es möglich, alle in einer Zeichenkette enthaltenen URLs zu kürzen. Zunächst muss der Service mittels eines gültigen API-Tokens initialisiert werden. Der Rückgabewert der Methode ShortenAllOccuringUrls(…) liefert ein Dictionary mit der ursprünglichen URL als Key und der gekürzten URL als Value.

Statische Methoden

void InitializeService(APIToken apiToken)
Dictionary<string, string> ShortenAllOccuringUrls(string textWithUrls)

Verwendete Klassen

StringClassExtensions


StringClassExtensions

Kurzbeschreibung

Diese Klasse erweitert die String Klasse um zwei weitere Methoden. Eine Methode dient dazu eine Zeichenkette in einzelne Wörter aufzuteilen und die andere Methode zählt die Wörter in einer Zeichenkette.

Erweiterungsmethoden

IEnumerable<string> SplitIntoWords(this string strToSplit)
int CountWords(this string strToCount)

RichTextBoxExtensions

Kurzbeschreibung

Diese Klasse erweitert die RichTextBox Klasse um vier weitere Methoden. Eine Methode dient dazu den Text aus dem FlowDocument zu extrahieren. Eine weitere Methode kann benutzt werden um den Text der RichTextBox zu setzten. Mit der Methode ReplaceText kann ein bestimmtes Textfragment in einer RichTextBox ersetzt werden. Mit der Methode CreateHyperlink können bestimmte Textfragmente (meistens URLs) in aktive Hyperlinks verwandelt werden, sowie ein EventHandler für das Hyperlink-Click-Event übergeben werden. Zusätzlich kann auch ein weiterer String übergeben werden, welcher die gekürzte URL enthält. Um die Methode CreateHyperlink so flexibel wie möglich zu halten, ist die Übergabe eine EventHandlers, sowie einer gekürzten URL optional.

Erweiterungsmethoden

string GetText(this RichTextBox richTextBox)
void SetText(this RichTextBox richTextBox, string text)
void ReplaceText(this RichTextBox richTextBox, string textToFind,
                 string replacementText)
void CreateHyperlink(this RichTextBox richTextBox, string hyperlinkText,
                     RoutedEventHandler hyperlinkClickedEventHandler = null,
                     string newHyperlinkText = "")

FindAndReplaceManager

Kurzbeschreibung

Diese Klasse wurde von Zhou Yong (http://shevaspace.blogspot.de) implementiert und enthält einige hilfreiche Methoden, mit welcher der Text in einem FlowDocument gesucht bzw. ersetzt werden kann.


ExtendedRichTextBox

Kurzbeschreibung

Diese Klasse wurde von Shawn Duggan (http://www.shawnduggan.com/ ) implementiert und stellt ein Steuerelement zur Verfügung, welches von der RichTextBox Klasse abgeleitet ist. Diese neue Klasse erweitert die RichTextBox Klasse um eine Text Eigenschaft. Da die RichTextBox der WPF nicht mehr über eine Text Eigenschaft verfügt, wie es bei WindowsForms noch der Fall war, kann man dieses Steuerelement verwenden und hat Zugriff auf den im FlowDocument enthaltenen Text. Da die Text Eigenschaft als DependencyProperty implementiert wurde, ist sogar eine Datenbindung problemlos möglich (wichtig für die Datenbindung an den TextBlock zur Anzeige der Zeichen).

XAML Namespaces

xmlns:controls="clr-namespace:SoftArcs.Community.ShawnDuggan.Windows.Controls;assembly=SoftArcs.Community"

Verwendung der Klasse

<controls:ExtendedRichTextBox x:Name="txtTweet" Grid.Row="1" Grid.ColumnSpan="2"
                              Margin="10,0" FontSize="14"
                              VerticalScrollBarVisibility="Auto" />

Der Quellcode aller Klassen sowie eine simple Beispielanwendung kann hier heruntergeladen werden :
http://dotnetmastermind.de/Dojo/TwitterUI.zip 
Download Source Code ... 
kick it on dotnet-kicks.de
WPF Login User Control mit “Windows 8 Look and Feel”

Auf CodeProject habe ich am heutigen Tage einen Artikel veröffentlicht, welcher die Implementierung eines sehr nützlichen WPF Benutzersteuerelementes detailliert beschreibt. Dieses WPF Benutzersteuerelement (WPF User Control) kann sehr einfach als Log-in-Overlay für eigene WPF-Anwendungen (ab. NET 3.5) verwendet werden. Es ähnelt dem Log-in Screen von Windows 8 und verhält sich auch nahezu identisch wie dieser.

Hier sind einige Screenshots des Benutzersteuerelementes :

Der vollständige Artikel ist verfügbar unter : http://www.codeproject.com/Articles/498253/Smart-WPF-Login-Overlay-Windows-8-Style (nur in Englisch)

Der Quellcode sowie eine Demo können unter dem angegebenen Link auch heruntergeladen werden. In der Demo ist ein Beispiel enthalten, wie das Benutzersteuerelement für eine "deutsche" Anwendung angepasst werden kann (siehe auch die obigen Screenshots).

EDIT : Bei dem UserControl mit den deutschen Texten muss folgende Eigenschaft noch gesetzt werden : FullSpan="On" (hatte ich versäumt … sorry). Und nicht wundern, dass das UserControl im WPF bzw. Blend - Designer transparent ist : Dies habe ich bewusst so implementiert, um zu zeigen, dass die Hintergrundfarbe des Root Fensters automatisch adaptiert wird, wenn auf dem UserControl keine Hintergrundfarbe gesetzt wurde.




kick it on dotnet-kicks.de

kick it on DotNetKicks.com
Improved Windows 8 PasswordBox Style for WPF

Regarding to the following blog post http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/06/windows-8-passwordbox-style-for-wpf.aspx i have furthermore improved the PasswordBox Style to look more similar to the Windows 8 PasswordBox. And again I did not use the VisualStateManager Class, so it is possible to use this style also with the .Net Framework 3.0 / 3.5 (and not only with the .Net Framework 4.0).

Here is how it looks like now :

This is the new corresponding style (Win8PasswordBoxStyle) :

<Style x:Key="Win8PasswordBoxStyle" TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource {x:Type PasswordBox}}">

    <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.ControlTextBrushKey}}"/>

    <Setter Property="Background" Value="{StaticResource {x:Static SystemColors.WindowBrushKey}}"/>

    <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>

    <Setter Property="FontFamily" Value="Times New Roman"/>

    <Setter Property="PasswordChar" Value="●"/>

    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>

    <Setter Property="BorderThickness" Value="1"/>

    <Setter Property="HorizontalContentAlignment" Value="Left"/>

    <Setter Property="VerticalContentAlignment" Value="Center"/>

    <Setter Property="Padding" Value="1"/>

    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>

    <Setter Property="AllowDrop" Value="true"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type PasswordBox}">

                <Border x:Name="OuterBorder"

                          BorderBrush="{TemplateBinding BorderBrush}"

                          BorderThickness="{TemplateBinding BorderThickness}"

                          Background="{TemplateBinding Background}"

                          SnapsToDevicePixels="true">

 

                    <Grid VerticalAlignment="Center">

                        <Grid.ColumnDefinitions>

                            <ColumnDefinition />

                            <ColumnDefinition Width="Auto" />

                        </Grid.ColumnDefinitions>

 

                        <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

                        <TextBox x:Name="RevealedPassword"

                                    Text="{TemplateBinding ap:PasswordBoxBinding.Password}"

                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                    VerticalContentAlignment="Center"

                                    Padding="{TemplateBinding Padding}"

                                    Margin="{TemplateBinding Padding}"

                                    Background="{TemplateBinding Background}"

                                    Visibility="Hidden" BorderThickness="0" IsReadOnly="True" FontFamily="Segoe UI" />

                        <TextBlock x:Name="TextBoxHint"

                                      Text="Enter password"

                                      Padding="{TemplateBinding Padding}"

                                      Background="Transparent"

                                      Cursor="IBeam" Margin="2" FontFamily="Segoe UI" Foreground="#FFB5B5B5"

                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                  Visibility="{Binding ElementName=RevealedPassword, Path=Text, Converter={StaticResource StringToOppositeVisibilityConverter}}" />

 

                        <Button x:Name="RevealButton"

                                  Grid.Column="1" SnapsToDevicePixels="True"

                                  Style="{StaticResource RevealButtonStyle}"

                              Visibility="{Binding ElementName=RevealedPassword, Path=Text, Converter={StaticResource StringToVisibilityConverter}}">

                        </Button>

                    </Grid>

                </Border>

 

                <ControlTemplate.Triggers>

                    <Trigger Property="IsEnabled" Value="false">

                    <Setter Property="Background" TargetName="OuterBorder" Value="{StaticResource {x:Static SystemColors.ControlBrushKey}}" />

                        <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.GrayTextBrushKey}}" />

                        <Setter Property="Opacity" TargetName="RevealButton" Value="0.5" />

                        <Setter Property="Text" TargetName="TextBoxHint" Value="{}{disabled} "/>

                    </Trigger>

 

                    <DataTrigger Binding="{Binding ElementName=RevealButton, Path=IsPressed}" Value="True">

                        <Setter TargetName="RevealedPassword" Property="Visibility" Value="Visible" />

                    </DataTrigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

 

… and this is the corresponding RevealButton-Style :

<Style x:Key="RevealButtonStyle" TargetType="{x:Type Button}">

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type Button}">

                <Grid>

                    <Border x:Name="PasswordRevealGlyphBorder" Background="Transparent" Margin="1,1,3,1"

                                BorderThickness="{TemplateBinding BorderThickness}">

                        <TextBlock x:Name="GlyphElement" Foreground="Black" VerticalAlignment="Center" HorizontalAlignment="Center"

                                        Text="&#xE052;" FontFamily="Segoe UI Symbol" Margin="3,0"

                                        FontSize="{TemplateBinding FontSize}" />

                    </Border>

                </Grid>

 

                <ControlTemplate.Triggers>

                    <DataTrigger Binding="{Binding ElementName=GlyphElement, Path=IsMouseOver}" Value="True">

                        <Setter TargetName="PasswordRevealGlyphBorder" Property="Background" Value="Gainsboro" />

                    </DataTrigger>

 

                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsPressed}" Value="True">

                        <Setter TargetName="PasswordRevealGlyphBorder" Property="Background" Value="Black" />

                        <Setter TargetName="GlyphElement" Property="Foreground" Value="White" />

                    </DataTrigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>


You can download the sample application here :

 
SmartPasswordBoxDemo2.zip



If you enjoyed reading the article, then please "Kick It" :

kick it on dotnet-kicks.de

kick it on DotNetKicks.com
Windows 8 PasswordBox Style for WPF

EDIT : Check out the improved style here http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/13/improved-windows-8-passwordbox-style-for-wpf.aspx

For everybody who is interested in a PasswordBox similar to the Windows 8 Login PasswordBox I have implemented a XAML-Style (available for WPF-Applications).

Here is how it looks like :

 

This is the corresponding style :

<Style x:Key="SmartPasswordBoxStyle" TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource {x:Type PasswordBox}}">

    <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.ControlTextBrushKey}}"/>

    <Setter Property="Background" Value="{StaticResource {x:Static SystemColors.WindowBrushKey}}"/>

    <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>

    <Setter Property="FontFamily" Value="Times New Roman"/>

    <Setter Property="PasswordChar" Value="●"/>

    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>

    <Setter Property="BorderThickness" Value="1"/>

    <Setter Property="HorizontalContentAlignment" Value="Left"/>

    <Setter Property="VerticalContentAlignment" Value="Center"/>

    <Setter Property="Padding" Value="1"/>

    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>

    <Setter Property="AllowDrop" Value="true"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type PasswordBox}">

                <Border x:Name="OuterBorder"

                          BorderBrush="{TemplateBinding BorderBrush}"

                          BorderThickness="{TemplateBinding BorderThickness}"

                          Background="{TemplateBinding Background}"

                          SnapsToDevicePixels="true">

                    <Grid VerticalAlignment="Center">

                        <Grid.ColumnDefinitions>

                            <ColumnDefinition />

                            <ColumnDefinition Width="Auto" />

                        </Grid.ColumnDefinitions>

                        

                        <ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

                        <TextBox x:Name="RevealedPassword"

                                    Text="{TemplateBinding ap:PasswordBoxBinding.Password}"

                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                    VerticalContentAlignment="Center"

                                    Padding="{TemplateBinding Padding}"

                                    Margin="{TemplateBinding Padding}"

                                    Background="{TemplateBinding Background}"

                                    Visibility="Hidden" BorderThickness="0" IsReadOnly="True" FontFamily="Segoe UI" />

                        <TextBlock x:Name="TextBoxHint"

                                      Text="Enter password"

                                      Padding="{TemplateBinding Padding}"

                                      Background="Transparent"

                                      Cursor="IBeam" Margin="2" FontFamily="Segoe UI" Foreground="#FFB5B5B5"

                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                                      Visibility="{Binding ElementName=RevealedPassword, Path=Text,

                                                    Converter={StaticResource StringToOppositeVisibilityConverter}}" />

                        

                        <Border x:Name="PasswordRevealImageBorder" Grid.Column="1" Margin="0,1,3,1"

                              Visibility="{Binding ElementName=RevealedPassword, Path=Text, Converter={StaticResource StringToVisibilityConverter}}">

                            <Image x:Name="PasswordRevealImage"

                                     Source="../../CommonImages/PasswordEyeBlack.png" Margin="2" />

                        </Border>

                    </Grid>

                </Border>

 

                <ControlTemplate.Triggers>

                    <Trigger Property="IsEnabled" Value="false">

                    <Setter Property="Background" TargetName="OuterBorder" Value="{StaticResource {x:Static SystemColors.ControlBrushKey}}"/>

                        <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.GrayTextBrushKey}}"/>

                        <Setter Property="Opacity" TargetName="PasswordRevealImageBorder" Value="0.5"/>

                        <Setter Property="Text" TargetName="TextBoxHint" Value="{}{disabled}"/>

                    </Trigger>

 

                    <DataTrigger Binding="{Binding ElementName=PasswordRevealImage, Path=IsMouseOver}" Value="True">

                        <Setter TargetName="PasswordRevealImageBorder" Property="Background" Value="Gainsboro" />

                    </DataTrigger>

 

                    <EventTrigger RoutedEvent="Image.MouseDown" SourceName="PasswordRevealImage">

                        <BeginStoryboard Storyboard="{StaticResource ShowPassword}"/>

                    </EventTrigger>

 

                    <EventTrigger RoutedEvent="Image.MouseUp" SourceName="PasswordRevealImage">

                        <BeginStoryboard Storyboard="{StaticResource HidePasswordAndChangeBorderColor}"/>

                    </EventTrigger>

 

                    <EventTrigger RoutedEvent="Image.MouseLeave" SourceName="PasswordRevealImage">

                        <BeginStoryboard Storyboard="{StaticResource HidePassword}"/>

                    </EventTrigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

 

As you can see you need a special AttachedProperty, some custom Storyboards an some Converters. So you have to reference the WPFSmartLibraryLight to make this working.

To give you an insight into the power of the WPFSmartLibrary I have put a link to a sample application at the end of this post including the demonstration of many cool functions. It is about a smart login dialog with almost no code behind (100% MVVM).

 

This sample application gives you a good overview to the following topics :

1. Very simple and easy to understand MVVM example

Login functionality with smart animations without code behind, except the following (needed) code behind :

    InitializeComponent();

    this.ViewModel = new LoginViewModel();

    this.DataContext = this.ViewModel;

 

=> Also no code behind for the animation needed (solved with AttachedProperties)

 

2. Good demonstration of some WPFSmartLibrary - features
  • SmartButtons
  • SmartBackgroundBrushes
  • WindowKeysHandling
  • TextBoxBaseManager
  • PasswordBoxManager
  • BoolToBrushConverter
  • BoolToVisibilityConverter
  • TextBoxBaseAnimation
  • VisibilityAnimation
  • PasswordBoxBinding
  • Validation Templates
  • and this everything in one small demo …

You can download the sample application here :

EDIT :  The source code has been updated, regarding issues with the WPF Designer ... sorry for possible inconvenience

  SmartPasswordBoxDemo.zip



If you enjoyed reading the article, then please "Kick It" :

kick it on dotnet-kicks.de

kick it on DotNetKicks.com
Die optimale Implementierung des INotifyPropertyChanged - Interfaces

Es existieren viele unterschiedliche Implementierungen des INotifyPropertyChanged - Interfaces.

Ausgehend von der allgemein bekannten Standard-Implementierung werden in diesem Beitrag die gängigsten Implementierungen analysiert sowie die Vor- und Nachteile aufgelistet. Zusätzlich wird eine eigene Implementierung, welche bereits in diversen größeren dotNet-Projekten verwendet wurde, vorgestellt. Außerdem kann am Ende des Beitrages eine ViewModelBase-Klasse (C#-Datei welche alle Implementierungen enthält) heruntergeladen werden. Diese darf darüber hinaus auch in eigenen Projekten verwendet werden.

[1] Die Verwendung der typischen Standard-Implementierung

public string someProperty;
public string SomeProperty
{
   get { return someProperty; }
   set
   {
      if (someProperty != value)
      {
         someProperty = value;
         RaisePropertyChangedEvent( "SomeProperty" );
      }
   }
}

Nachteile

  1. Der übergebene String-Parameter wird bei der Eingabe nicht überprüft wird und man kann sich somit unbemerkt vertippen => mögliche Fehlerquelle.
  2. Bei einer Änderung des Bezeichners könnte man vergessen den String-Parameter mit anzupassen, dies würde auch unbemerkt bleiben => mögliche Fehlerquelle.
  3. Es muss immer ein extra Feld als "Datenspeicher" implementiert werden.
  4. Das PropertyChanged-Event muss manuell gefeuert werden.

 

[2] Die Verwendung einer verbesserten Standard-Implementierung

Hierbei wird zusätzlich überprüft, ob eine Eigenschaft mit der Bezeichnung des übergebenen String-Parameters existiert :

[Conditional("DEBUG")]
private void checkPropertyName(string propertyName)
{
   PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties( this )[propertyName];
   if (propertyDescriptor == null )
   {
      string message = string.Format( null, "The property with the propertyName '{0}' doesn't exist.", propertyName );
      Debug.Fail( message );
   }
}

Nachteile

  1. Alle Nachteile der Standardlösung, jedoch mit kleinen Unterschied, dass beim Debuggen festgestellt werden kann, ob eine Eigenschaft mit dem übergebenen Bezeichner auch wirklich existiert.


[3] Die Verwendung einer nochmals verbesserten Standard-Implementierung

Hierbei wird der Parameter als Lambda-Expression übergeben :

public string someProperty;
public string SomeProperty
{
   get { return someProperty; }
   set
   {
      if (someProperty != value )
      {
         someProperty = value;
         RaisePropertyChangedEvent( () => SomeProperty );
      }
   }
}

Vorteile

  1. Die in den ersten beiden Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, weil der Bezeichner in der Lambda Expression eine existierende Eigenschaft sein muss, da ansonsten die IDE (Visual Studio) bzw. spätestens der Compiler feststellen würde, dass eine Eigenschaft mit diesem Bezeichner nicht existiert.
  2. Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst.

Nachteile

  1. Es muss immer ein extra Feld als "Datenspeicher" implementiert werden.
  2. Das PropertyChanged-Event muss manuell gefeuert werden.

 

[4] Die Verwendung einer nahezu optimalen Standard-Implementierung

Hierbei wird das Event ohne der Übergabe eines Parameters gefeuert (identisch zu der Vorgehensweise im dotNet-Framework 4.5 bzw. C# 5.0) :

public string someProperty;

public string SomeProperty

{

    get { return someProperty; }

    set

    {

        if (someProperty != value)

        {

            someProperty = value;

            RaisePropertyChangedEvent();

        }

    }

}

 

Implementierung

protected void RaisePropertyChangedEvent()

{

    // Get the call stack

    StackTrace stackTrace = new StackTrace();

 

    // Get the calling method name

    string callingMethodName = stackTrace.GetFrame( 1 ).GetMethod().Name;

 

    // Check if the callingMethodName contains an underscore like in "set_SomeProperty"

    if (callingMethodName.Contains( "_" ))

    {

        // Extract the property name

        string propertyName = callingMethodName.Split( '_' )[1];

 

        if (this.PropertyChanged != null && propertyName != String.Empty)

        {

            this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );

        }

    }

}

 

Vorteile

  1. Die in den ersten beiden Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, da kein Parameter mehr übergeben werden muss.
  2. Eine Änderung des Bezeichners wirkt sich auch nicht problematisch aus, da wie gesagt kein Parameter übergeben wird.

Nachteile

  1. Es muss immer noch ein extra Feld als "Datenspeicher" implementiert werden.
  2. Das PropertyChanged-Event muss weiterhin manuell gefeuert werden.

 

[5] Die (meiner Meinung nach) optimale Implementierung

Hierbei wird jede Property auf die gleiche Weise, wie die zu den DependencyProperties gehörenden CLR-Wrapper implementiert. Bei den (zu den DependencyProperties) gehörenden CLR-Wrappern wird durch die Verwendung der Methoden GetValue bzw. SetValue das Property-System des .NET-Frameworks aktiv und führt diverse Evaluierungen im Hintergrund durch. Eine identische Vorgehensweise habe ich für eine optimale Implementierung des INotifyPropertyChanged-Interfaces verwendet.

Die Verwendung

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}

Vorteile

  1. Die in den ersten beiden Standard-Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, weil der Bezeichner in der Lambda Expression eine existierende Eigenschaft sein muss, da ansonsten die IDE (Visual Studio) bzw. spätestens der Compiler feststellen würde, dass eine Eigenschaft mit diesem Bezeichner nicht existiert
  2. Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst
  3. Ein extra Feld als "Datenspeicher" muss nicht mehr implementiert werden, da die Werte der einzelnen Eigenschaften automatische in einem Dictionary der ViewModelBase-Klasse verwaltet werden
  4. Das Event muss nicht mehr manuell gefeuert werden, da dieses automatisch in der SetValue-Methode der ViewModelBase-Klasse erfolgt
  5. Im Gegensatz zu den DependencyProperties des .NET-Frameworks muss der Rückgabewert der GetValue-Methode nicht mehr gecastet werden, da der Rückgabewert typensicher aus der Lambda-Expression ermittelt wird

Nachteile

  1. KEINE

 

Da die (meiner Ansicht nach) optimale Implementierung ähnlich der Implementierung von DependencyProperties funktioniert, ist eine noch bessere Konsistenz in der Code Basis gewährleistet.

Implementierung

public abstract class ViewModelBase : INotifyPropertyChanged

{

    #region < INotifyPropertyChanged > Members

 

    /// <summary>

    /// Is connected to a method which handle changes to a property (located in the WPF Data Binding Engine)

    /// </summary>

    public event PropertyChangedEventHandler PropertyChanged;

 

    /// <summary>

    /// Raise the [PropertyChanged] event

    /// </summary>

    /// <param name="propertyName">The name of the property</param>

    protected void OnPropertyChanged(string propertyName)

    {

        if (this.PropertyChanged != null)

        {

            this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );

        }

    }

 

    #endregion

 

    private Dictionary<string, object> propertyValueStorage;

 

    #region Constructor

 

    public ViewModelBase()

    {

        this.propertyValueStorage = new Dictionary<string, object>();

    }

 

    #endregion

 

    /// <summary>

    /// Set the value of the property and raise the [PropertyChanged] event

    /// (only if the saved value and the new value are not equal)

    /// </summary>

    /// <typeparam name="T">The property type</typeparam>

    /// <param name="property">The property as a lambda expression</param>

    /// <param name="value">The new value of the property</param>

    protected void SetValue<T>(Expression<Func<T>> property, T value)

    {

        LambdaExpression lambdaExpression = property as LambdaExpression;

 

        if (lambdaExpression == null)

        {

            throw new ArgumentException( "Invalid lambda expression", "Lambda expression return value can't be null" );

        }

 

        string propertyName = this.getPropertyName( lambdaBLOCKED EXPRESSION;

 

        T storedValue = this.getValue<T>( propertyName );

 

        if (!object.Equals( storedValue, value ))

        {

            this.propertyValueStorage[propertyName] = value;

            this.OnPropertyChanged( propertyName );

        }

    }

 

    /// <summary>

    /// Get the value of the property

    /// </summary>

    /// <typeparam name="T">The property type</typeparam>

    /// <param name="property">The property as a lambda expression</param>

    /// <returns>The value of the given property (or the default value)</returns>

    protected T GetValue<T>(Expression<Func<T>> property)

    {

        LambdaExpression lambdaExpression = property as LambdaExpression;

 

        if (lambdaExpression == null)

        {

            throw new ArgumentException( "Invalid lambda expression", "Lambda expression return value can't be null" );

        }

 

        string propertyName = this.getPropertyName( lambdaBLOCKED EXPRESSION;

 

        return getValue<T>( propertyName );

    }

 

    /// <summary>

    /// Try to get the value from the internal dictionary of the given property name

    /// </summary>

    /// <typeparam name="T">The property type</typeparam>

    /// <param name="propertyName">The name of the property</param>

    /// <returns>Retrieve the value from the internal dictionary</returns>

    private T getValue<T>(string propertyName)

    {

        object value;

 

        if (propertyValueStorage.TryGetValue( propertyName, out value ))

        {

            return (T)value;

        }

        else

        {

            return default( T );

        }

    }

 

    /// <summary>

    /// Extract the property name from a lambda expression

    /// </summary>

    /// <param name="lambdaExpression">The lambda expression with the property</param>

    /// <returns>The extracted property name</returns>

    private string getPropertyName(LambdaExpression lambdaExpression)

    {

        MemberExpression memberExpression;

 

        if (lambdaExpression.Body is UnaryExpression)

        {

            var unaryExpression = lambdaExpression.Body as UnaryExpression;

            memberExpression = unaryExpression.Operand as MemberExpression;

        }

        else

        {

            memberExpression = lambdaExpression.Body as MemberExpression;

        }

 

        return memberExpression.Member.Name;

    }

}

 

Die ViewModelBase-Klasse kann man sich hier herunterladen :

  ViewModelBase.zip



Falls euch der Artikel gefallen sollte, dann bitte "kicken" :

kick it on dotnet-kicks.de

Gloweffekt in .NET 4.0 (BitmapEffect-Klasse)
Seit .NET 4.0 läuft die Zuweisung eines „OuterGlowBitmapEffektes“ ins Leere.

Das Ärgerliche bei dem Versuch diese Klasse zu verwenden ist, dass man im XAML-Code zunächst einmal nichts bemerkt (kein Hinweis im Visual Studio auf eine obsolete [veraltete bzw. ungültig gewordene] Klasse und auch keine Exception beim Ausführen der Applikation (ein Exception-Handling ist im XAML-Code ja nur bedingt möglich). Somit überprüft man erst andere mögliche Fehlerquellen um dann festzustellen, dass sowohl die BitmapEffect-Klasse als auch die OuterGlowBitmapEffect-Klasse weiterhin in .NET 4.0 existent sind, jedoch bei einer Zuweisung der gewünschte Effekt ausbleibt.

Im MSDN habe ich diesbezüglich folgenden Artikel gefunden :
http://msdn.microsoft.com/en-us/library/ms743435.aspx 

Da ich diesen Effekt jedoch für meine WPFSmartButtons benötige, habe ich als Workaround stattdessen die DropShadowEffect-Klasse verwendet, welche von der Effect-Klasse abgeleitet ist.

Statt z.B.
<OuterGlowBitmapEffect GlowColor="White" GlowSize="10" />

habe ich
<DropShadowEffect ShadowDepth="0" Color="White" BlurRadius="30" RenderingBias="Quality"/>

verwendet.

Der Effekt ist nahezu identisch, auch wenn geringfügige optische Abweichungen bestehen.

Screenshots der Beispielapplikation :




Bei Fragen und Anregungen schreibt mir doch einfach einen Kommentar oder eine PN.

Die Beispielapplikation könnt ihr euch hier herunterladen :

  BitmapEffect.zip

kick it on dotnet-kicks.de
Posted: Dez 30 2010, 12:51 von TheArchitect | mit 1 comment(s)
Abgelegt unter: , , , ,
Silverlight Applikation pausieren ohne dass der Browser „einfriert“
Zum Pausieren einer Silverlight Applikation kann man nicht einfach die Methode Thread.Sleep( int millisecondsTimeout ) aus dem Namespace System.Threading verwenden, da dadurch nicht nur die Silverlight Applikation, sondern darüber hinaus auch der ganze Browser (in welchem die Applikation ja als Client ausgeführt wird) einfriert.

Als Workaround habe ich stattdessen ein Storyboard verwendet, welches eine frei bestimmbare Zeit einfach gar nichts macht. Dann habe ich dieses Storyboard in eine Methode verpackt, welche folgendermaßen aussieht :

private void SLThreadSleep( int TimeoutInMilliseconds )
{
    // Storyboard für eine Pause von x Millisekunden erzeugen ...
    Storyboard SleepStoryboard = new Storyboard();
    DoubleAnimation SleepDuration = new DoubleAnimation() { Duration = TimeSpan.FromMilliseconds( TimeoutInMilliseconds ), From = 1, To = 1 };
    SleepStoryboard.Children.Add( SleepDuration );
    Storyboard.SetTarget( SleepDuration, LayoutRoot );
    Storyboard.SetTargetProperty( SleepDuration, new PropertyPath( "(UIElement.Opacity)" ) );
    SleepStoryboard.Completed += new EventHandler( (ds, de) =>
        {
        // Master Grid und gesamten Inhalt wieder aktivieren
        LayoutRoot.IsHitTestVisible = true;
        // Das Rücksetzen des Buttontextes ist nur für die Beispielapplikation gedacht
        btnSleep2.Content = "SL App 5 Sekunden pausieren (mit Storyboard)";
        } );
    SleepStoryboard.Begin();
}


(Bei LayoutRoot handelt es sich um das sogenannte Master-Grid, welches sich im Logical Tree an oberster Stelle befindet)

Bei Fragen und Anregungen schreibt mir doch einfach einen Kommentar oder eine PN.

Die Beispielapplikation könnt ihr euch hier herunterladen :

  Sl4ThreadSleep.zip

kick it on dotnet-kicks.de
Posted: Dez 30 2010, 12:33 von TheArchitect | mit no comments
Abgelegt unter: , ,
TextBlock animiert ein- und ausblenden
Nachdem ich gestern bei einem Film die Einblendungen der „credits“ im Vorspann sah, dachte ich mir : „Das geht doch bestimmt auch ganz einfach mit WPF“.

Habe mich dann auch dran gesetzt und eine kleine Extension-Methode geschrieben, mit welcher man jeden beliebigen TextBlock um eine „credits“-ähnliche Animation erweitern kann. Die Extension-Methode sieht folgendermaßen aus :

public static void AddSmartFadingAnimation( this TextBlock TextBlockToAnimate,
                                            List<string> FadingTextList,
                                            double TextPromptDuration,
                                            double FadingSpeed )

Die Verwendung der Methode ist recht einfach. Man legt einfach ein TextBlock-Control in seiner GUI an, weist dem TextBlock-Control alle gewünschten Eigenschaften (Properties) zu, erstellt ein String-Collection (List<sring>), füllt diese mit den entsprechenden Texten ( .add(„Text“) ) und wendet die (Extension-)Methode AddSmartFadingAnimation(...) auf dem TextBlock-Objekt an. Eine beispielhafte Verwendung der Methode sieht folgendermaßen aus :

List<string> TextList = new List<string>();
for( int i = 0; i < 10; i++ )
  TextList.Add( "Einzublendender Text Nr. " + i );

tblFadingInfo.AddSmartFadingAnimation( TextList, 1000, 1500 );

(tblFadingInfo ist hierbei der Name eines beispielhaften TextBlock-Objektes)


Screenshots der Beispielapplikation :






Bei Fragen und Anregungen schreibt mir doch einfach einen Kommentar oder eine PN.

Die Beispielapplikation könnt ihr euch hier herunterladen :

  SmartTextFading.zip



kick it on dotnet-kicks.de
Silverlight 4 Templates für WP7 Apps
Als Ergänzung zum meinem Blog-Eintrag vom 22.11.2010 (http://dotnet-forum.de/blogs/thearchitect/archive/2010/11/22/windows-phone-7-entwicklung-best-practice.aspx) hier nochmals die Möglichkeit zum Download eines Projektes, welches ein einfaches Template sowie ein erweitertes Template (mit einigen Controls und dem Wechsel zwischen einzelnen Seiten) enthält.

Die Templates wurden bewusst sehr einfach gehalten, um als Vorlage für die weitere Entwicklung zu dienen (es sind also keine Storyboards oder Animationen enthalten).

Screenshots des einfachen Templates ( SL4 - WP7 ) :




Screenshots des erweiterten Templates ( SL4 ) :




Screenshots des erweiterten Templates ( WP7 ) :



Wie man erkennen kann, sind optische Unterschiede kaum noch vorhanden.

Wichtig ist es jedoch die nachfolgenden Unterschiede zwischen der SL4 App und der WP7 App zu beachten.

In der WMAppManifest.xml folgende Änderung vornehmen :

<Tasks>
<DefaultTask  Name ="_default" NavigationPage="Page1.xaml"/>
</Tasks>


Das URI-Mapping muss in die App.xaml verlagert werden und zusätzlich muss folgender Code in den Konstruktor der App.xaml.cs eingefügt werden :

RootFrame.UriMapper = Resources["UriMapper"] as UriMapper;


Bei Fragen und Anregungen schreibt mir doch einfach einen Kommentar oder eine PN.

Das Projekt mit den beiden Templates könnt ihr euch hier herunterladen :

  SL4WP7Templates.zip


  WP7ExtTemplate.zip

kick it on dotnet-kicks.de
Windows Phone 7 Entwicklung “Best Practice“
Bei einer kürzlich durchgeführten Entwicklung einer grafischen Oberfläche (GUI) für eine Windows Phone 7 (WP7) App bin ich folgendermaßen vorgegangen :

Für die Entwicklung einer WP7 App muss man die Windows Phone Developer Tools RTW installieren. Diese sind verfügbar unter:
http://www.microsoft.com/downloads/details.aspx?FamilyID=04704acf-a63a-4f97-952c-8b51b34b00ce&displayLang=de

Die Tools bestehen aus folgenden Komponenten :

•    Visual Studio 2010 Express for Windows Phone
•    Windows Phone Emulator Resources
•    Silverlight 4 Tools für Visual Studio
•    XNA Game Studio 4.0
•    Microsoft Expression Blend for Windows Phone


Dies bedeutet jedoch, dass man auf jeden Fall neben seinem produktiv genutzten Visual Studio noch eine zweite Express-Version installieren müsste. Da die Installation von mehreren Visual Studio Versionen in manchen Fällen (nicht immer!) zu Problemen führen kann, habe ich mir eine virtuelle Maschine eingerichtet, welche als Entwicklungsplattform dienen sollte. Auf diese habe ich dann Windows 7 Professional 32 Bit und die Windows Phone Developer Tools RTW installiert.

Bei ersten Tests fielen mir sofort folgende Nachteile des WP7 Emulators auf :
  • Der Emulator ist nicht gerade der schnellste (ich dachte erst, weil er in der virtuellen Maschine läuft, aber die anderen Programme starten fast genauso schnell wie im Host, also liegt es am Emulator)
  • GUI-Tests mit vielen Texteingaben sind sehr mühselig, da diese immer über die virtuelle Phone Tastatur erfolgen müssen und nicht über die normale Tastatur erfolgen können
Um die WP7 App also effizient entwickeln zu können, musste ich mir etwas einfallen lassen.

So kam es, dass ich in meinem Host (dem produktiven Entwicklungsrechner) eine Silverlight 4 (SL4) Applikation erstellt habe, welche als Grundlage für die WP7 App dienen sollte.

Um die GUI-Identität zu wahren, musste ich zunächst einmal die optischen Vorgaben einer WP7 App in SL4 abbilden. Hierzu mussten einige Vorarbeiten (das Anlegen von Systemressourcen) geleistet werden.

Folgende Eigenschaften muss das “User Control“ Element besitzen, damit eine WP7 App in SL4 nachgebildet werden kann :

<UserControl … Height="768" Width="480" FontFamily="Segoe UI"
FontSize="20" Foreground="White">


Die Größe des Fenster sollte bei einer „normalen“ Phone 7 Applikation eine Breite von 480px und eine Höhe von 768px haben, bei einer sogenannten „Panorama“ Application liegen die Werte bei einer Breite von 480px und einer Höhe von 800px (in diesem Beitrag wird lediglich die „normale“ Applikation besprochen). Die Nachbildung der drei weiteren Systemressourcen erreicht man mit der Zuweisung der Font-Family „Segoe UI“, einer Font-Size von "20" und der Foreground-Farbe „White“.
 
Darüber hinaus musste ich die von jeder WP7 App genutzten Font-Systemressourcen in SL4 verfügbar machen. Also habe ich die Font-Systemressourcen in Form von „Application.Resourcen“ nachgebildet :

<Application.Resources>
<!-- Dies sind Nachbildungen der Standard WP7-Systemressourcen -->
<Style x:Key="PhoneTextNormalStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="20"/>
</Style>

<Style x:Key="PhoneTextTitle1Style" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Light"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="72"/>
</Style>

<Style x:Key="PhoneTextTitle2Style" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Light"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="32"/>
</Style>
</Application.Resources>


Somit war es mir möglich, diese Ressourcen den TextBlock-Controls zuzuordnen, ohne diese in WP7 nochmals ändern zu müssen (da dort ja identische Systemressourcen existieren) und dennoch eine identische Optik zu erreichen.

Eine in beiden Entwicklungsumgebungen gültige Zuweisung lautet dann :

<TextBlock x:Name="ApplicationTitle" Text="Name der App"
Style="{StaticResource PhoneTextNormalStyle}"/>


Wie gesagt behält diese Zuweisung ihre Gültigkeit auch in der WP7 Entwicklungsumgebung und damit hält man auch die Microsoft GUI Design Standards für die WP7 App Entwicklung ein, welche das Nutzen dieser Systemressourcen ausdrücklich empfehlen.

Nun konnte ich also die WP7 App in SL4 effizient auf meinem schnellen Produktiv-Rechner entwickeln und nicht gezwungenermaßen auf der doch schon etwas langsamerem virtuellen Maschine mit den oben genannten Nachteilen.

Auf diese Weise war es mir möglich die GUI der nachfolgend dargestellten WP7 App voll funktionsfähig innerhalb von nur 5 (!) Tagen inkl. aller dazugehörigen Funktionalitäten wie Datenbindung, DataTemplates, Überblendeffekten, Storyboards, dynamischer Anzeige, Dual-Language-Support etc. fertig zu stellen.

Screenshots der WP7 App :






Das Silverlight-Pendant sieht folgendermaßen aus :





Wie man sieht, sind die optischen Unterschiede nur noch marginal.

Bei Fragen und Anregungen schreibt mir doch einfach einen Kommentar oder eine PN.

Die Vorlage für eine WP7 App in Form eines Silverlight 4 Projektes könnt ihr hier herunterladen :

  SL4WP7Template.zip

kick it on dotnet-kicks.de