Archive

Archive for the ‘Softwareentwicklung’ Category

Rahmenlose Fenster mit WPF

April 20th, 2009 Bernd 5 comments

NonRectWindow

Achtung, das folgende Beispiel bringt wahrscheinlich keinen Nutzen für den täglichen Umgang mit WPF!

EGAL! Manchmal muss man eben auch spielen :-) . Rahmenlose Fenster sind einfach cool. Wenn dann auch noch ein guter Designer mit im Spiel ist, sieht das Ganze auch besser aus als bei meinem Versuch.

Ok, Design beiseite. Was ist zu tun um eine Anwendung mit einem rahmenlosen Fenster zu erstellen?

Der erste Schritt besteht darin, an dem Fenster der Applikation drei Eigenschaften entsprechend zu setzen:

  • AllowsTransparency auf true
  • WindowStyle auf None – in Verbindung mit AllowsTransparency=”True” wird erreicht, dass der Fensterrahmen und die Titelzeile verschwinden
  • Background auf Transparent
    <Window x:Class="NonRectShapedWindowWPF.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="215"
        Height="215"
        AllowsTransparency="True"
        WindowStyle="None"
        Background="Transparent">
    ...

    In meinem Beispiel habe ich mich dazu entschieden das UI aus zwei Kreisen zusammenzusetzen. Der grosse Kreis dient als “Hauptrahmen” der Anwendung und der kleine Kreis enthält einen “Close” Button.

  • ...
    <Grid Height="200" Width="200">
        <Ellipse Fill="Red" Stroke="DarkRed"
                 MouseLeftButtonDown="OnCircleMouseLeftButtonDown">
            <Ellipse.BitmapEffect>
                <OuterGlowBitmapEffect GlowSize="8" GlowColor="OrangeRed" />
            </Ellipse.BitmapEffect>
        </Ellipse>
    
        <Canvas...>
    
        <Button Margin="150 -150 0 0"
                Template="{StaticResource closeButton}"
                FontFamily="Webdings"
                FontWeight="Bold"
                FontSize="10"
                Content="r"
                Click="OnCloseButtonClick"
                ToolTip="Close">
        </Button>
    </Grid>
    ...

Der eigentliche Inhalt der Anwendung versteckt sich in dem zusammengeklappten <Canvas> Element. Die Details dazu gibt es weiter unten.

Wie in dem XAML Code zu sehen ist, wird der “Close” Button über die Margin Eigenschaft an der gewünschten Stelle positioniert. Um dem Button ein rundes Aussehen zu verpassen, habe ich ein ControlTemplate erstellt.

...
<ControlTemplate x:Key="closeButton" TargetType="{x:Type Button}">
    <Grid>
        <Ellipse Fill="Red" Stroke="DarkRed"
                 Width="25" Height="25"/>

        <Label Content="{TemplateBinding Content}"
               Foreground="Black"
               HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
</ControlTemplate>
...

Um dem Anwender ein Verschieben des Fensters zu ermöglichen, fügt man einen Eventhandler für das MouseLeftButtonDown Event hinzu. Und zwar an dem Element, mit dem das Fenster verschoben werden soll. Das könnte z.B. eine eigene Titelleiste sein, oder wie bei mir irgendein anderes, sichtbares Element. Der Code in dem Eventhandler ist sehr einfach:

private void OnCircleMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    DragMove();
}

Ein weiterer Eventhandler wird für das Click Event unseres “Close” Buttons benötigt um das Fenster zu schliessen:

private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
    Close();
}

Damit sind die wichtigsten Schritte gemacht, um ein rahmenloses Fenster mit WPF zu erstellen. Es kann vom Anwender verschoben und geschlossen werden. Um ein bisschen Action in das Beispiel zu bringen, gibt es noch eine kleine Animation, die Text durch das Fenster scrollen lässt. Der Code dazu sieht so aus:

...
<Canvas Height="100" Width="120" ClipToBounds="True">
    <TextBlock x:Name="_text"
               Height="100" Width="120"
               VerticalAlignment="Center" HorizontalAlignment="Center"
               TextWrapping="Wrap"
               FontFamily="Tahoma" FontSize="12">
            <TextBlock.Text>
                Diese Anwendung ist völlig sinnlos. Zu nichts zu gebrauchen. Und doch hat es viel Spass
                gemacht sie zu erstellen.
            </TextBlock.Text>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="TextBlock.Loaded" >
                  <BeginStoryboard>
                    <Storyboard>
                      <DoubleAnimation
                        Storyboard.TargetName="_text"
                        Storyboard.TargetProperty="(Canvas.Top)"
                        Duration="0:0:4"
                        From="110" To="-80"
                        RepeatBehavior="Forever"/>
                    </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
    </TextBlock>
</Canvas>
...

Das komplette Beispiel kann hier heruntergeladen werden:

Databinding und Validierung in WPF – Validation Rules

March 24th, 2009 Bernd 4 comments

In dem letzten Posting zu dem Thema habe ich einige Vorbereitungen getroffen, um Data Validation unter die Lupe zu nehmen. Eine simple Beispielanwendung wurde vorgestellt, die als Basis für weitere Untersuchungen dient. Diesmal geht es um

Validation Rules

Hier eine Übersicht der eingebauten Validation Rules:

ClassDiagramValidationRules

Validation Rules werden direkt am Binding angegeben. Für die beiden mitgelieferten Rules gibt es zwei Möglichkeiten diese zu aktivieren.

Einmal über die Eigenschaft ValidationRules:

...
<TextBox.Text>
    <Binding Source="{StaticResource cd}"
             Path="Year"
             UpdateSourceTrigger="PropertyChanged">
        <Binding.ValidationRules>
            <ExceptionValidationRule/>
            <DataErrorValidationRule/>
        </Binding.ValidationRules>
    </Binding>
</TextBox.Text>
...

Oder explizit über die beiden Eigenschaften ValidatesOnExceptions und ValidatesOnDataErrors:

...
<TextBox.Text>
    <Binding Source="{StaticResource cd}"
             Path="Year"
             UpdateSourceTrigger="PropertyChanged"
             ValidatesOnExceptions="True"
             ValidatesOnDataErrors="True">
    </Binding>
</TextBox.Text>
...
Zur Erinnerung: in dem Beispiel aus dem vorherigen Posting ist eine Exception aufgetreten, wenn alle Zeichen in der TextBox gelöscht wurden. Hier kommt die ExceptionValidationRule ins Spiel. Diese Regel fängt alle Exceptions ab, die während der Aktualisierung der Datenquelle auftreten. Schauen wir uns also an, was passiert wenn in dem Beispiel diese Regel aktiviert wird.
 
ExceptionValidationRule aktiv

 

WPF teilt uns mit, dass ein ungültiger Wert eingegeben wurde, indem ein roter Rahmen um die TextBox gezeichnet wird. Das ist die Standardvisualisierung von WPF um Validierungsfehler anzuzeigen.

ErrorTemplates

Mit Hilfe eines eigenen ErrorTemplates ist es möglich, dem Anwender eine hilfreichere Visualisierung für Eingabefehler zu bieten. Das könnte z.B. so aussehen:

ExceptionValidationRule mit eigenem ErrorTemplate aktiv

Aha, zumindest wird jetzt ein erklärender Text angezeigt. Die Wortwahl gefällt mir noch nicht besonders, was daran liegt, dass einfach nur der Exceptiontext der gefangenen FormatException angezeigt wird. Das werden wir später ändern. Wie sieht nun das ErrorTemplate aus?

...
<ControlTemplate x:Key="TextBoxErrorTemplate">
    <DockPanel>
        <TextBlock Margin="0 0 5 0"
                   Text="!"
                   FontSize="14"
                   FontWeight="Bold"
                   Foreground="Red"/>

        <AdornedElementPlaceholder x:Name="adornedElement"/>

        <TextBlock Text="{Binding ElementName=adornedElement,
                                  Path=AdornedElement.(Validation.Errors),
                                  Converter = {local:ValidationErrorToErrorMessageConverter}}"
                   FontSize="12"
                   Foreground="Red"
                   Margin="5 0 0 0"/>
    </DockPanel>
</ControlTemplate>
...

Im Mittelpunkt steht das zu dekorierende Element (in diesem Fall die TextBox). Über das Tag AdornedElementPlaceholder bestimmt man, an welcher Stelle im Template dieses Element platziert werden soll. Mit Hilfe eines DockPanels wird zuerst das rote Ausrufezeichen, dann die TextBox und zum Schluss noch ein TextBlock mit dem Fehlertext angezeigt. Der Fehlertext wird über Databinding von der Attached Property Validation.Errors der TextBox geholt. Das Databinding System fügt dieser Liste ein ValidationError Objekt hinzu, wenn eine Validation Rule einen Fehler zurückgibt.

Zusätzlich wird hier noch ein Value Converter verwendet. Dieser bringt die Fehler aus der Liste der ValidationErrors in ein lesbares Format (siehe hierzu auch Josh Smith’s Post für eine andere Variante). Der Code hierzu sieht so aus:

[ValueConversion(typeof(ReadOnlyObservableCollection<ValidationError>), typeof(string))]
public class ValidationErrorToErrorMessageConverter : MarkupExtension, IValueConverter
{
    private ValidationErrorToErrorMessageConverter _mySelf;

    public override object ProvideValue( IServiceProvider serviceProvider )
    {
        if (_mySelf == null)
        {
            _mySelf = new ValidationErrorToErrorMessageConverter();
        }
        return _mySelf;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        ReadOnlyObservableCollection<ValidationError> errors =
            value as ReadOnlyObservableCollection<ValidationError>;

        StringBuilder errorMessage = new StringBuilder();
        if(null != errors)
        {
            foreach (var error in errors)
            {
                errorMessage.AppendLine(error.ErrorContent.ToString());
            }

            return errorMessage;
        }

        return errorMessage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Der Value Converter leitet sich zusätzlich noch von MarkupExtension ab. Dadurch ist es nicht mehr nötig den Converter als Ressource anzugeben, sondern dieser kann direkt über die Markupextension Syntax verwendet werden (danke an Dr. WPF für diesen Tipp).

Das ErrorTemplate wird der TextBox folgendermaßen bekanntgemacht gemacht (siehe Zeile 5):

...
<TextBox ToolTip="Please enter year of release"
         Width="60"
         Margin="10"
         Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}">
    <TextBox.Text>
        <Binding Source="{StaticResource cd}"
                 Path="Year"
                 UpdateSourceTrigger="PropertyChanged"
                 ValidatesOnExceptions="True">
        </Binding>
    </TextBox.Text>
</TextBox>
...

Selbstgeschriebene Validation Rules

Zusätzlich zu den mitgelieferten Validation Rules hat man natürlich die Möglichkeit eigene Rules zu implementieren. Dazu wird die eigene Klasse von ValidationRule abgeleitet und die Methode Validate implementiert. Eine Validation Rule um zu prüfen, ob in unserem Beispiel die eingegebene Jahreszahl innerhalb des gewünschten Zeitraums ist, könnte so aussehen:

public class NumberInRangeValidationRule : ValidationRule
{
    public int MaxValue { get; set; }
    public int MinValue { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        Debug.WriteLine("entering NumberInRangeValidationRule.Validate");

        // InvalidCast Exception is wanted if this rule is used the wrong way.
        int intValue = (int)value;

        if( intValue < MinValue || intValue > MaxValue )
        {
            return new ValidationResult(false,
                                        string.Format("Please enter a number between {0} and {1}.",
                                                      MinValue, MaxValue));
        }

        return ValidationResult.ValidResult;
    }
}

Diese Validation Rule bietet die Möglichkeit über zwei Eigenschaften den gewünschten Wertebereich anzugeben. In der Validate Methode wird dann überprüft, ob der eingegebene Wert innerhalb dieses Bereichs ist. Ist das nicht der Fall, wird ein ValidationResult mit einem entsprechenden Text zurückgegeben. Warum aber der Test, ob der übergebene Wert vom Typ int ist (Zeile 10)? Schauen wir uns mal an, wie diese Rule im XAML verwendet wird:

...
<TextBox.Text>
    <Binding Source="{StaticResource cd}"
             Path="Year"
             UpdateSourceTrigger="PropertyChanged"
             ValidatesOnExceptions="True">
        <Binding.ValidationRules>
            <local:NumberInRangeValidationRule MinValue="1980"
                                               MaxValue="2009"
                                               ValidationStep="ConvertedProposedValue"/>
        </Binding.ValidationRules>
    </Binding>
</TextBox.Text>
...

Ab .Net 3.5 SP1 gibt es die Eigenschaft ValidationStep an der Klasse ValidationRule. Darüber kann man angeben, zu welchem Zeitpunkt während des Databindingvorganges die Validierung erfolgen soll. Es stehen vier Möglichkeiten zur Auswahl:

  1. RawProposedValue – Das ist der Defaultwert. Die Validierung wird vorgenommen bevor der Wert konvertiert wird. In dem Beispiel mit der TextBox muss damit ein String validiert werden (die Text Eigenschaft ist vom Typ String).
  2. ConvertedProposedValue – Bei dieser Einstellung wird der konvertierte Wert validiert. Im Beispiel also ein Integer.
  3. UpdatedValue – Die Validierung erfolgt nachdem die Quelle aktualisiert wurde.
  4. CommittedValue – Die Validierung erfolgt nachdem der Wert an die Quelle übergeben wurde.

Die letzten beiden Optionen sind mir ein bisschen suspekt. Mir ist kein Anwendungsfall eingefallen und der Unterschied zwischen beiden ist mir auch nicht klar.

    Die oben gezeigte NumberInRangeValidationRule arbeitet auf Integer Werten, weshalb ich “ConvertedProposedValue” als ValidationStep angegeben habe. Bei der Implementierung ist eine InvalidCastException gewollt, damit eine falsche Benutzung sofort bemerkt wird.
    So, jetzt fehlt nur noch ein schönerer Text für den Fall, dass keine Zahl sondern irgendwas anderes eingegeben wird. Dazu schreiben wir noch eine IsValidIntegerValidationRule:

    public class IsValidIntegerValidationRule : ValidationRule
    {
        public override ValidationResult Validate( object value, System.Globalization.CultureInfo cultureInfo )
        {
            Debug.WriteLine( "entering IsValidIntegerValidationRule.Validate" );
            string inputString = value as string;
    
            if (null != inputString)
            {
                int inputNumber;
                if( false == int.TryParse(inputString, out inputNumber ) )
                {
                    return new ValidationResult( false, "Please enter a valid number." );
                }
            }
    
            return ValidationResult.ValidResult;
        }
    }

    Für diese Validation Rule belassen wir den ValidationStep beim Default, also “RawProposedValue”. Damit sieht die finale Konfiguration für das Databinding so aus:

    ...
    <TextBox ToolTip="Please enter year of release"
             Width="60"
             Margin="10"
             Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}">
        <TextBox.Text>
            <Binding Source="{StaticResource cd}"
                     Path="Year"
                     UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <local:NumberInRangeValidationRule MinValue="1980"
                                                       MaxValue="2009"
                                                       ValidationStep="ConvertedProposedValue"/>
                    <local:InputMustBeIntegerValidationRule ValidationStep="RawProposedValue"/>
    
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    ...

    Uns so sieht das Ganze dann im UI aus:

Invalid input format

Input out of range

    Jetzt gibt es noch ein Problem, für das ich bisher noch keine zufriedenstellende Lösung gefunden habe. Beim Start der Anwendung wird über das TwoWay Binding der Initiale Wert aus der Quelle geholt. Dieser Wert ist bei einem Integer 0. Der Wert 0 liegt nicht im Wertebereich der NumberInRangeValidationRule, es wird aber keine Validierungsmeldung angezeigt. Es sieht so aus, also ob die Validierung nur von Ziel zu Quelle durchgeführt wird. Hier bin ich für Ideen bzw. Lösungsvorschläge sehr dankbar.

Der nächste Post zum Thema Validierung mit WPF widmet sich dem IDataErrorInfo Interface. Die Unterstützung dafür wurde mit .Net 3.5 SP1 in WPF eingeführt. Ich bin gespannt.

Das komplette Beispiel kann hier heruntergeladen werden:

WPF Databinding in Verbindung mit einem Defaultbutton

March 10th, 2009 Bernd 2 comments

Vor kurzem ist ein Kollege von mir auf ein kleines Problem mit Databinding gestoßen. Und zwar in Verbindung mit einem Button dessen “IsDefault” Eigenschaft auf True gesetzt ist. Es wurde die Bindingquelle nicht aktualisiert.

Zur Verdeutlichung soll folgendes UI dienen:

Defaultbutton with databinding

Die TextBox für den Username ist über Databinding an die “Username” Eigenschaft des ViewModels angebunden. Bei dem Ok Button ist “IsDefault” auf True gesetzt. Mit Hilfe einiger Traces kann man im Ausgabefenster des Visual Studio sehen, was vom Benutzer eingegeben wurde.

...
public string Username
{
    get { return _userName; }
    set
    {
        _userName = value;
        Trace.WriteLine(string.Format("Username {0} entered.", _userName));
    }
}
...
private void OnOkCommand()
{
    Trace.WriteLine("Ok button pressed.");
    Trace.WriteLine(string.Format("Current username {0}", _userName));

}
...
Wenn nach der Eingabe eines Benutzernamens der Ok Button mit der Maus geklickt wird, klappt alles wie erwartet. Der Setter der Username Eigenschaft wird aufgerufen und dannach die Methode OnOkCommand() ausgeführt. Wird allerdings direkt nach der Eingabe die Enter Taste gedrückt, wird zwar OnOkCommand() ausgeführt, aber nicht der Username Setter. Was ist hier los?

Bei einer TextBox die per Databinding angebunden ist, wird die Quelle per Default bei Verlust des Fokus aktualisiert. D.h. die Eigenschaft “UpdateSourceTrigger” des Bindings hat den Wert “LostFocus”. Scheinbar wird beim Auslösen eines Defaultbuttons der Fokus nicht auf diesen Button gesetzt. Es wird dann zwar das Command ausgeführt welches mit dem Button verknüpft ist, aber die Quelle der TextBox wurde nicht aktualisiert.

Es gibt mehrere Möglichkeiten das Problem zu lösen.

  1. Durch Setzen der “UpdateSourceTrigger” Eigenschaft am Binding auf “PropertyChanged”. Dadurch wird die Quelle bei jeder Veränderung aktualisiert.
  2. Durch manuelles Setzen des Fokus im Click-Eventhandler des Buttons
  3. Durch manuelles Aktualisieren des Bindings im Click-Eventhandler des Buttons

Bei Variante zwei muss der entsprechende Code in die Code-Behind Datei der View verlagert werden, da ein direkter Zugriff auf das Button Control notwendig ist:

private void OnOkButtonClick(object sender, RoutedEventArgs e)
{
    FocusManager.SetFocusedElement(this, (Button)sender);
}

Den Code für die dritte Lösung könnte man (technisch) auch ins ViewModel packen. Gefällt mir aber nicht, da ich im ViewModel keine Kenntnis von UI Controls haben möchte. Deshalb kommt auch hier die Code-Behind Datei zu Zuge:

private void OnOkButtonClick(object sender, RoutedEventArgs e)
{
    TextBox focusedTextBox = Keyboard.FocusedElement as TextBox;
    if( null != focusedTextBox )
    {
        BindingExpression bindingExpression = focusedTextBox.GetBindingExpression(TextBox.TextProperty);
        if (null != bindingExpression)
        {
            bindingExpression.UpdateSource();
        }
    }
}

Bisher gefällt mir die zweite Variante am Besten. Mir sind auch noch keine Nachteile oder Probleme aufgefallen. Den “richtigen” Code, der beim Auslösen des Defaultbuttons ausgeführt werden soll, kann man im ViewModel lassen. Das Command welches mit dem Button verknüpft ist, wird erst nach dem Click Event ausgeführt.

Weiss zufällig jemand, ob es diesen Effekt mit dem Defaultbutton und LostFocus auch unter WinForms gibt?

Categories: Softwareentwicklung Tags:

Unterschiede zwischen WPF und Silverlight

February 28th, 2009 Bernd No comments

Microsoft hat im Rahmen der “Composite Application Guidance for WPF and Silverlight” (Prism v2) eine kompakte Sammlung der Unterschiede zwischen WPF und Silverlight bereitgestellt. Es werden die Bereiche Resources, Styles, Trigger, Data Binding, Commanding und noch Verschiedenes angesprochen. Sehr nützlich um einen schnellen Überblick zu bekommen.

Uncle Bob schreibt über SOLID

February 15th, 2009 Bernd No comments

Robert C. Martin alias Uncle Bob gibt Tipps zum erfolgreichen Start mit den SOLID Prinzipien. SOLID besteht aus fünf Prinzipien, die uns Entwicklern helfen wartbaren, lesbaren, erweiterbaren, kurz qualitativ hochwertigen Code zu schreiben.

In der vor kurzem ins Leben gerufenen Initiative Clean Code Developer (CCD), sind alle diese Prinzipien in den verschiedenen Graden verankert. Dort findet man noch viel mehr Informationen zu professioneller Softwareentwicklung – nix wie hin!