Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1908, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Disabilita cookie ShinyStat

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Xamarin Certified Mobile Developer

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

Mostrare messaggi di validazione sui dati in WPF

TRADUZIONE AUTORIZZATA DA BETH MASSI. POST ORIGINALE A QUESTO INDIRIZZO

 

Come probabilmente avete visto dagli ultimi post, ho lavorato con WPF in diversi scenari orientati ai dati. Ieri stavo analizzando la validazione dei dati in WPF e .NET 3.5, che è abbastanza facile. In questo articolo spiegherò come ottenere la validazione dei dati utilizzando l’interfaccia IDataErrorInfo, per poi occuparmi di un paio di ErrorTemplates per la validazione che potete usare per visualizzare messaggi di errore nella validazione e indicazioni all’utente.

 

Validare gli oggetti/dati

Se utilizzate oggetti business personalizzati o classi LINQ to SQL, per prima cosa è necessario implementare l’interfaccia IDataErrorInfo al fine di aggregare messaggi di validazione sui vostri oggetti. Se state usando i DataSet in WPF o Windows Form, l’oggetto DataRowView già implementa quest’interfaccia, quindi potete semplicemente aggiungere la validazione alle classi parziali relative alla vostra DataTable e siete a posto. Aprite il designer dei DataSet, tasto destro del mouse sulla DataTable e selezionate “View Code” per iniziare. Per esempio, se avessimo una DataTable per i clienti (Customer) potremmo scrivere la validazione per il campo LastName in questo modo:

 

Partial Class CustomerDataSet

    Partial Class CustomerDataTable

 

        Private Sub CheckLastName(ByVal row As CustomerRow)

            If row.IsNull("LastName") OrElse row.LastName = "" Then

                row.SetColumnError(Me.LastNameColumn, "Please enter a last name")

            Else

                row.SetColumnError(Me.LastNameColumn, "")

            End If

        End Sub

 

        Private Sub CustomerDataTable_ColumnChanged(ByVal sender As Object, _

                                                    ByVal e As System.Data.DataColumnChangeEventArgs) _

                                                    Handles Me.ColumnChanged

            If e.Column Is Me.LastNameColumn Then

                Me.CheckLastName(CType(e.Row, CustomerRow))

            End If

        End Sub

 

 

        Private Sub CustomerDataTable_TableNewRow(ByVal sender As Object, _

                                                  ByVal e As System.Data.DataTableNewRowEventArgs) _

                                                  Handles Me.TableNewRow

            Dim row As CustomerRow = CType(e.Row, CustomerRow)

            'This will fire the ColumnChanged event which will give

            'immediate feedback to user when a row is added.

            '(Stick other default values here too.)

            row.LastName = ""

        End Sub

    End Class

 

End Class

 

Se state progettando i vostri oggetti business personalizzati o state usando classi LINQ to SQL, è necessario implementare IDataErrorInfo manualmente. Ho mostrato come fare ciò con LINQ to SQL in questo post in cui ho predisposto una classe business base. Qui c’è una versione ridotta, un esempio di implementazione di tipo customer in una classe LINQ to SQL che esegue la stessa validazione sul campo LastName:

 

Partial Class Customer

    Implements System.ComponentModel.IDataErrorInfo

 

    'This dictionary contains a list of our validation errors for each field

    Private validationErrors As New Dictionary(Of String, String)

 

    Protected Sub AddError(ByVal columnName As String, ByVal msg As String)

        If Not validationErrors.ContainsKey(columnName) Then

            validationErrors.Add(columnName, msg)

        End If

    End Sub

 

    Protected Sub RemoveError(ByVal columnName As String)

        If validationErrors.ContainsKey(columnName) Then

            validationErrors.Remove(columnName)

        End If

    End Sub

 

    Public Overridable ReadOnly Property HasErrors() As Boolean

        Get

            Return (validationErrors.Count > 0)

        End Get

    End Property

 

    Public ReadOnly Property [Error]() As String _

        Implements System.ComponentModel.IDataErrorInfo.Error

        Get

            If validationErrors.Count > 0 Then

                Return String.Format("{0} data is invalid.", TypeName(Me))

            Else

                Return Nothing

            End If

        End Get

    End Property

 

    Default Public ReadOnly Property Item(ByVal columnName As String) As String _

        Implements System.ComponentModel.IDataErrorInfo.Item

        Get

            If validationErrors.ContainsKey(columnName) Then

                Return validationErrors(columnName).ToString

            Else

                Return Nothing

            End If

        End Get

    End Property

 

    Private Sub OnValidate(ByVal action As System.Data.Linq.ChangeAction)

        Me.CheckLastName(Me.LastName)

 

        If Me.HasErrors Then

            Throw New Exception(Me.Error)

        End If

    End Sub

 

    Private Sub OnLastNameChanging(ByVal value As String)

        Me.CheckLastName(value)

    End Sub

 

    Private Sub CheckLastName(ByVal value As String)

        If value = "" Then

            Me.AddError("LastName", "Please enter a last name")

        Else

            Me.RemoveError("LastName")

        End If

    End Sub

    

End Class

 

Data Binding in WPF

 

Ora che i nostri oggetti per i dati supportano la validazione, possiamo collegarli a una finestra. Predisporre una semplice finestra WPF con alcune TextBox e collegarle scrivendo XAML è facile se si ha l’abilità di ricordare la sintassi ;-) Il punto focale è assicurarsi di specificare l’attributo ValidatesOnDataErrors nel Binding e impostarlo a True. Considerate le TextBox nel seguente XAML:

 

<Window x:Class="Window1"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Title="Customers" Height="253" Width="300" Name="Window1">

    <Grid Margin="6">

        <Grid.RowDefinitions>

            <RowDefinition Height="222*" />

            <RowDefinition Height="40*" />

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="112*" />

            <ColumnDefinition Width="166*" />

        </Grid.ColumnDefinitions>

        <StackPanel Name="StackPanel1">

            <Label Name="Label1"

                   Width="Auto"

                   HorizontalContentAlignment="Right"

                   Margin="3">

                Last Name:</Label>

            <Label Name="Label2"

                   Width="Auto"

                   HorizontalContentAlignment="Right"

                   Margin="3">

                First Name:</Label>

            <Label Name="Label3"

                   Width="Auto"

                   HorizontalContentAlignment="Right"

                   Margin="3">

                City:</Label>

            <Label Name="Label4"

                   Width="Auto"

                   HorizontalContentAlignment="Right"

                   Margin="3">

                State:</Label>

            <Label Name="Label5"

                   Width="Auto"

                   HorizontalContentAlignment="Right"

                   Margin="3">

                ZIP:</Label>

        </StackPanel>

        <StackPanel Grid.Column="1" Name="StackPanel2">

            <TextBox

                Text="{Binding Path=LastName, ValidatesOnDataErrors=True}"

                Name="TextBox1"

                Height="28"

                Width="Auto"

                HorizontalContentAlignment="Left"

                Margin="3" />

            <TextBox

                Text="{Binding Path=FirstName, ValidatesOnDataErrors=True}"

                Name="TextBox2"

                Height="28"

                Width="Auto"

                HorizontalContentAlignment="Left"

                Margin="3" />

            <TextBox

                Text="{Binding Path=City, ValidatesOnDataErrors=True}"

                Name="TextBox3"

                Height="28"

                Width="Auto"

                HorizontalContentAlignment="Left"

                Margin="3"/>

            <TextBox

                Text="{Binding Path=State, ValidatesOnDataErrors=True}"

                Name="TextBox4"

                Height="28"

                Width="Auto"

                HorizontalContentAlignment="Left"

                Margin="3"/>

            <TextBox

                Text="{Binding Path=ZIP, ValidatesOnDataErrors=True}"

                Name="TextBox5"

                Height="28"

                Width="Auto"

                HorizontalContentAlignment="Left"

                Margin="3" />

        </StackPanel>

        <Button Name="btnAdd"

                Grid.Column="1" Grid.Row="1"

                Margin="0,0,79,6"

                Height="24" Width="75"

                VerticalAlignment="Bottom"

                HorizontalAlignment="Right" >Add</Button>

        <Button Name="btnSave"

                Grid.Column="1" Grid.Row="1"

                HorizontalAlignment="Right"

                Margin="0,0,0,6"

                Width="75" Height="24"

                VerticalAlignment="Bottom">Save</Button>

    </Grid>

</Window>

 

Ora possiamo caricare i nostri dati e assegnarli alla proprietà Window.DataContext nel gestore di evento Window_Loaded. Se utilizzate i DataSet, predisponete normalmente la vostra query sulla TableAdapter e popolate il DataSet con Fill. Quindi assegnate la DataTable customer alla Window.DataContext:

 

Me.CustomerTableAdapter.Fill(Me.MyCustomerData.Customer)

Me.DataContext = Me.MyCustomerData.Customer

 

Se state usando LINQ to SQL, potete lavorare direttamente sul DataContext per caricare l’elenco di clienti:

 

Dim db As New MyDatabaseDataContext

Me.DataContext = From Customer In db.Customers _

                 Where Customer.LastName = "Massi"

 

ErrorTemplate di default per la validazione in WPF

 

Se mandiamo in esecuzione l’applicazione così com’è, verrebbe visualizzata un’indicazione di default quando la validazione fallisce. Il controllo viene bordato di rosso e ciò indica che c’è un problema, tuttavia nessun messaggio viene mostrato. Oh si, davvero utile! Preparatevi, le linee telefoniche del vostro supporto tecnico diventeranno infuocate quando rilascerete l’applicazione!

 

 

Specifare uno stile di validazione personalizzato

 

Ovviamente vogliamo far sì che l’utente sappia cosa è necessario correggere. Rendiamo le cose semplici e mostriamo un messaggio in una ToolTip. Per ora possiamo creare uno Style nelle risorse della finestra (Window.Resources) che influenzi le TextBox sulla stessa. Lo Style espone un Trigger che imposta le proprietà della ToolTip con il messaggio di validazione quando Validation.HasErrors diventa True:

 

<Window.Resources>

    <Style TargetType="TextBox">

        <Style.Triggers>

            <Trigger Property="Validation.HasError" Value="true">

                <Setter Property="ToolTip"

                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},

                        Path=(Validation.Errors)[0].ErrorContent}"/>

            </Trigger>

        </Style.Triggers>

    </Style>

</Window.Resources>

 

Se proviamo ad eseguire di nuovo il codice, vedremo che quando passiamo sopra la TextBox col mouse il messaggio di validazione viene mostrato in una ToolTip. Decisamente migliore! Ma questa soluzione va bene per le TextBox. Come si fa per altri controlli come CheckBox, ComboBox, ecc.? Inoltre vogliamo davvero dichiarare tutto questo in un unico posto per tutta l’applicazione. Nessun problema, possiamo muovere questo Style all’interno delle Application.Resources. Possiamo anche specificare TargetType=”Control” a quindi possiamo dichiarare ulteriori stili per i rimanenti controlli e basare questi sul primo. Apriamo il file Application.Xaml, aggiungendo il seguente codice nella sezione delle risorse:

 

<Application.Resources>

    <Style TargetType="Control" x:Key="myErrorTemplate">

        <Style.Triggers>

            <Trigger Property="Validation.HasError" Value="true">

                <Setter Property="ToolTip"

                    Value="{Binding RelativeSource={x:Static RelativeSource.Self},

                    Path=(Validation.Errors)[0].ErrorContent}"/>

            </Trigger>

        </Style.Triggers>

    </Style>

    <Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}" />

    <Style TargetType="CheckBox" BasedOn="{StaticResource myErrorTemplate}" />

    <Style TargetType="ComboBox" BasedOn="{StaticResource myErrorTemplate}" />

</Application.Resources>

 

Dobbiamo solo specificare un tag x:Key per il Template, quindi possiamo impostare l’attributo BasedOn sullo stile da cui si eredita. Ora tutti i controlli dell’applicazione possono fruire dello stile.

 

Sostituire l’intero ErrorTemplate

 

Finora tutto ciò che abbiamo fatto è stato specificare uno Style Trigger. L’ErrorTemplate di default in WPF è tuttora utilizzato, come si può vedere dal bordo rosso intorno ai controlli. Possiamo modificare completamente l’ErrorTemplate che è stato utilizzato definendo un modello nuovo qui nelle Application.Resources. Facciamo un piccolo esempio modificando il nostro ErrorTemplate affinché mostri un messaggio generico sul controllo. Nello stile, sopra la sezione Triggers (lasceremo lì il messaggio della ToolTip) impostiamo la proprietà Validation.ErrorTemplate e il suo valore sul nostro ControlTemplate.

 

<Style TargetType="Control" x:Key="myErrorTemplate">

        <Setter Property="Validation.ErrorTemplate">

            <Setter.Value>

                <ControlTemplate>

                    <TextBlock Foreground="Red" Text="DOH! Thank you for trying."/>

                </ControlTemplate>

            </Setter.Value>

        </Setter>

        <Style.Triggers>

        <Trigger Property="Validation.HasError" Value="true">

            <Setter Property="ToolTip"

                Value="{Binding RelativeSource={x:Static RelativeSource.Self},

                Path=(Validation.Errors)[0].ErrorContent}"/>

        </Trigger>

    </Style.Triggers>

</Style>

 

Ora, se mandiamo in esecuzione il codice, otterremo ancora la ToolTip passando col mouse sul controllo ma stavolta stiamo anche coprendo il TextBlock che abbiamo definito nel ControlTemplate. Notate che non c’è più il bordo rosso:

 

 

Okay, un esempio abbastanza banale, lo ammetto. Il problema (sarcasmo a parte) è che il TextBlock sta davvero coprendo il controllo e bisogna passare col mouse sopra il margine per poter visualizzare la ToolTip. Un altro problema è sicuramente il fatto che, se iniziamo a scrivere nel campo, il messaggio non sparirà finché non lo eliminiamo a mano, quindi un po’ noioso.

Però si può utilizzare un DockPanel all’interno del ControlTemplate e fissare il TextBlock sulla destra al fine di mostrare il testo dopo il controllo (e stavolta ci limitiamo a mostrare un asterisco). Supponiamo di voler ancora avere il bordo rosso intorno al controllo. Possiamo fare questo specificando uno speciale element chiamato AdornedElementPlaceholder nel nostro XAML per la proprietà ErrorTemplate Setter.Value:

 

<Setter Property="Validation.ErrorTemplate">

    <Setter.Value>

        <ControlTemplate>

            <DockPanel LastChildFill="True">

                <TextBlock DockPanel.Dock="Right"

                        Foreground="Red"

                        FontSize="11pt"

                        FontWeight="Bold">*

                </TextBlock>

                <Border BorderBrush="Red" BorderThickness="1">

                    <AdornedElementPlaceholder Name="myControl"/>

                </Border>

            </DockPanel>

        </ControlTemplate>

    </Setter.Value>

</Setter>

 

 

Molto meglio! Sicuramente potete usare la vostra immaginazione per creare qualunque tipo di indicazione che ritenete appropriata per le vostre applicazioni. E’ il bello di WPF.

 

Riprodurre il Look & Feel dell’ErrorProvider di Windows Forms

 

Per gli sviluppatori Windows Forms che passano di qua, come si fa a riprodurre il look & feel dell’ErrorProvider, che mostra un’icona lampeggiante per gli errori? Mi è sempre piaciuto posizionare l’icona rossa di errore all’interno della parte destra del controllo, quindi non mi dovevo preoccupare delle problematiche di spazio tra controlli nel predisporre i form. Ed inoltre mi piaceva il fatto che l’icona lampeggiasse alcuni secondi e poi basta. E’ relativamente semplice creare questo tipo di animazioni in WPF utilizzando gli StoryBoard (ed è DAVVERO semplice creare animazioni con Expression Blend, quindi raccomando vivamente di dare un’occhiata al prodotto se state migrando verso WPF).

Questa volta creeremo un Ellipse e imposteremo un EventTrigger per l’evento Loaded al fine di avviare l’animazione che si occuperà semplicemente di alternare la proprietà Visibility dell’Ellipse per alcuni secondi. Vogliamo inoltre posizionare un TextBlock sopra l’Ellipse e il suo testo sarà un punto esclamativo (l’animazione verrà eseguita su questo). E poiché vogliamo posizionare questi all’interno della parte destra del controllo impostando un margine sinistro negativo, vogliamo anche impostare le ToolTip per l’Ellipse e il TextBlock così che, se l’utente passa col mouse sull’errore, potrà visualizzare la ToolTip.

Qui c’è lo XAML completo per riprodurre questo look & feel, contenuto nelle Application.Resources:

 

<Application.Resources>

    <Storyboard x:Key="FlashErrorIcon">

        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00"

                                       Storyboard.TargetProperty="(UIElement.Visibility)">

            <DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Hidden}"/>

            <DiscreteObjectKeyFrame KeyTime="00:00:00.2000000" Value="{x:Static Visibility.Visible}"/>

            <DiscreteObjectKeyFrame KeyTime="00:00:00.4000000" Value="{x:Static Visibility.Hidden}"/>

            <DiscreteObjectKeyFrame KeyTime="00:00:00.6000000" Value="{x:Static Visibility.Visible}"/>

            <DiscreteObjectKeyFrame KeyTime="00:00:00.8000000" Value="{x:Static Visibility.Hidden}"/>

            <DiscreteObjectKeyFrame KeyTime="00:00:01" Value="{x:Static Visibility.Visible}"/>

        </ObjectAnimationUsingKeyFrames>

    </Storyboard>

    <Style x:Key="myErrorTemplate" TargetType="Control">

        <Setter Property="Validation.ErrorTemplate">

            <Setter.Value>

                <ControlTemplate>

                    <DockPanel LastChildFill="True">

                        <Ellipse DockPanel.Dock="Right"

                                 ToolTip="{Binding ElementName=myTextbox,

                                     Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"

                                 Width="15" Height="15"

                                 Margin="-25,0,0,0"

                                 StrokeThickness="1" Fill="Red" >

                            <Ellipse.Stroke>

                                <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">

                                    <GradientStop Color="#FFFA0404" Offset="0"/>

                                    <GradientStop Color="#FFC9C7C7" Offset="1"/>

                                </LinearGradientBrush>

                            </Ellipse.Stroke>

                            <Ellipse.Triggers>

                                <EventTrigger RoutedEvent="FrameworkElement.Loaded">

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

                                </EventTrigger>

                            </Ellipse.Triggers>

                        </Ellipse>

                        <TextBlock DockPanel.Dock="Right"

                                ToolTip="{Binding ElementName=myControl,

                                     Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"

                                Foreground="White"

                                FontSize="11pt"

                                Margin="-15,5,0,0" FontWeight="Bold">!

                            <TextBlock.Triggers>

                                <EventTrigger RoutedEvent="FrameworkElement.Loaded">

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

                                </EventTrigger>

                            </TextBlock.Triggers>

                        </TextBlock>

                        <Border BorderBrush="Red" BorderThickness="1">

                            <AdornedElementPlaceholder Name="myControl"/>

                        </Border>

                    </DockPanel>

                </ControlTemplate>

            </Setter.Value>

        </Setter>

        <Style.Triggers>

            <Trigger Property="Validation.HasError" Value="true">

                <Setter Property="ToolTip"

                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},

                        Path=(Validation.Errors)[0].ErrorContent}"/>

            </Trigger>

        </Style.Triggers>

    </Style>

    <Style TargetType="TextBox" BasedOn="{StaticResource myErrorTemplate}" />

    <Style TargetType="CheckBox" BasedOn="{StaticResource myErrorTemplate}" />

    <Style TargetType="ComboBox" BasedOn="{StaticResource myErrorTemplate}" />

</Application.Resources>

 

Ora possiamo eseguire la nostra applicazione e innescare l’errore di validazione; vedremo un’icona di errore che lampeggia per 3 volte (apparirà più fluida che non nella seguente immagine ;-))

 

 

Validare gli oggetti per i dati in WPF con .NET Framework 3.5 è lo stesso di quanto si faceva con WinForms utilizzando l’interfaccia IDataErrorInfo. Tuttavia, gli stili e i control template di WPF rendono molto flessibile la visualizzazione di indicazioni. Ciò che immaginate, potete probabilmente farlo con WPF.

 

Enjoy!

 

(post originale di Beth Massi qui)

 

Alessandro

Print | posted on domenica 26 aprile 2009 00:16 | Filed Under [ Visual Basic Windows Presentation Foundation ]

Powered by:
Powered By Subtext Powered By ASP.NET