WPF – Enlace de datos

Descripción general

En este documento, que se puede clasificar de pequeño manual, se estudia cómo establecer un enlace de datos de entre una clase y unos controles de un formulario Wpf. Se describe la manera de realizar el enlace y como resolver los problemas más comunes que se presentan, como son: Establecer el aspecto grafico de los controles ante un error de entrada, como definir y establecer las reglas de validación de los datos introducidos, y como recuperar la información introducida en el formulario

[TOC] Tabla de Contenidos


↑↑↑

WPF – Enlace de datos


↑↑↑

Pasos a dar

  1. Definir en el formulario el aspecto visual de los errores
  2. Declarar una clase de tipo POCO (Plain Old CLR Objects) que mapee los datos que se introducen por el formulario y que contenga las reglas de negocio de los mismos.
  3. Enlazar la clase al formulario, las PROPIEDADES (No está en mayúscula por casualidad, solo se enlazan las propiedades) de la clase a los controles del formulario
  4. Declarar un método que cuando acabe toda la introducción de datos volverá a examinar las entradas (No es estrictamente necesario pero si conveniente)
  5. Recuperar la instancia de la clase POCO con los datos introducidos en el formulario para usarlos en algún otro sitio.


↑↑↑

Aspecto inicial del formulario

Para simplificar supongamos un formulario en el que se entra un nombre de fichero y un directorio, y tendrá (más o menos este aspecto)

Imagen 01

Imagen 01 - Aspecto inicial del formulario


↑↑↑

Clase POCO

Nuestra clase que mapea este formulario y que se llama [DatosMapeadosFormulario] es la siguiente:

Imagen 02

Imagen 02 - Propiedades de la clase que mapea el formulario

El código completo de la clase está más adelante en el apartado 'Código de este ejemplo'


↑↑↑

Definir el aspecto grafico de los controles ante un error

En primer lugar y para poder explicar luego todo seguido el proceso del enlace de datos, escribo aquí el código xaml que cambia el aspecto del control cundo hay un error, es una forma de avisar gráficamente de que existe algún problema

El aspecto que presentan estas modificaciones consiste en mostrar un carácter admiración (!) delante del control, cambiar el color de fondo del control y cargar en el ToolTip la descripción del problema. El aspecto de una pantalla mostrando un error es el siguiente:

Imagen 03

Imagen 03 - Aspecto del formulario ante un error de introducción de datos

Y el código necesario para realizarlas es el siguiente

      <Window.Resources>
        <namespaceLocal:DatosMapeadosFormulario  x:Key="IntanciaDatosMapeadosFormulario"/>


        <!-- Estilos que se aplican cuando hay un error  -->
        <Style x:Key="estilosParaErrorDelTextBox" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                            Path=(Validation.Errors)[0].ErrorContent}"/>

                    <Setter Property="Background" Value="Yellow" />
                    <Setter Property="BorderBrush" Value="Red" />

                </Trigger>
            </Style.Triggers>
        </Style>

        <!-- Plantilla que simula un error provider -->
        <ControlTemplate x:Key="templateSimularUnFormErrorProvider">
            <DockPanel>
                <!--<Label  Foreground="Red" Background="Azure"  Content="(!)" />-->
                <TextBlock Foreground="Red" FontSize="20"> (!)</TextBlock>
                <AdornedElementPlaceholder />
            </DockPanel>
        </ControlTemplate>

    </Window.Resources>


↑↑↑

Proceso de enlace de datos

Para enlazar esa clase hay que dar varios pasos,


↑↑↑

A) Declarar el namespace

En primer lugar declarar el "namespace" que se va a usar, es decir el espacio de nombres donde se encuentra la clase. Como en este ejemplo esta en el mismo espacio de nombres que el formulario lo definiremos con el nombre de "local" y eso se hace en la definición de la ventana.

<Window x:Class="WindowPrueba"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:namespaceLocal="clr-namespace:BindValidation_VB"
    Title="WindowPrueba" Height="232" Width="339">

Observa que:


↑↑↑

B) Declarar la instancia del objeto

A continuación hay que declarar la clase que se va a usar, y eso se hace en [Window.Resources] de la siguiente manera

    <Window.Resources>
        <namespaceLocal:DatosMapeadosFormulario  
                        x:Key="IntanciaDatosMapeadosFormulario"/>

         <!--Aquí va todo el código de formateo de controles para mostrar errores -->

    </Window.Resources>

Observa que:


↑↑↑

C) Definir y enlazar el TextBox donde se recoge el Nombre del fichero

        <Label Height="28" HorizontalAlignment="Left" 
               Margin="36,24,0,0" VerticalAlignment="Top" 
               Content="Introduce un nombre completo de fichero"  
               Name="LabelTituloNombreFichero"  />

        <TextBox Name="textBoxNombreFichero" FontSize="15" Margin="40,51,65,97"   
                  Style="{StaticResource estilosParaErrorDelTextBox}"  
                  Validation.ErrorTemplate="{StaticResource templateSimularUnFormErrorProvider}" 
                  VerticalAlignment="Top">

            <TextBox.Text>
                <Binding Source="{StaticResource IntanciaDatosMapeadosFormulario}"
                         Path="NombreFichero" 
                         UpdateSourceTrigger="LostFocus">
                    <Binding.ValidationRules>
                        <!-- Las reglas de validación especificas de este dato -->
                        <namespaceLocal:ReglasValidacionNombreFicheroSimples />
                        <!-- Observa que no se emplean las reglas estándar 
                             (que están comentadas a continuación)-->
                        <!--<ExceptionValidationRule />-->
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>

Observa que en la definición anterior están incluidos:

1) Estilos gráficos

Que estilos gráficos se aplican cuando hay errores

Con esta instrucción se aplican los estilos gráficos (previamente definidos en [Windows.Resources]) que mostrara al Control TextBox si ocurre algún error

     Style="{StaticResource estilosParaErrorDelTextBox}"  

2) Plantillas se emplean.

En este caso se emplea la plantilla (previamente definida en [Windows.Resources]), que dibuja sobre la marcha un control [TextBlock] (lee bien TextBlok y no TextBox). El contenido de este control por su forma y color recuerda el aviso del control Windows.Forms:ErrorProvider)

     Validation.ErrorTemplate="{StaticResource templateSimularUnFormErrorProvider}"

3) El enlace de datos propiamente dicho

                <Binding Source="{StaticResource IntanciaDatosMapeadosFormulario}"
                         Path="NombreFichero" 
                         UpdateSourceTrigger="LostFocus">

UpdateSourceTrigger (Enumeración)

Puedes encontrar más información en la documentación MSDN de Microsoft en la voz [UpdateSourceTrigger (Enumeración)]

En un enlace a datos, la idea es que siempre que exista una modificación de los datos, se comunique al "escuchador de los mismos" (al origen de enlace), en general se puede hacer de varias maneras, cada vez que se modifica el campo, [PropertyChanged] (por ejemplo, cada vez que escribimos un carácter), cuando salimos del control [LostFocus] o bajo demanda [Explicit] estos valores están incluidos en la enumeración

El valor [Default] se aplica cada control e indica cómo se comporta ese control por defecto. Por ejemplo, no tiene mucho sentido que en un control TextBox se efectúe un enlace de datos cada vez que se introduce un carácter en el control. El comportamiento por defecto es el de [LostFocus] es decir hacer únicamente un enlace cuando se pierda el foco. Aunque si se declara la propiedad [UpdateSourceTrigger] se cambia el comportamiento.

El valor [Explicit] indica que se actualiza origen de enlace sólo cuando se llama al método de [UpdateSource] del enlace. Esto hay que hacerlo necesariamente desde código y hay que dar dos pasos

  1. Recuperar un objeto [BindingExpresion] que contenga el valor de [Binding] que se ha definido en el código xaml para ese control
  2. Llamar explícitamente al método BindingExpresion.UpdateSource

Un pequeño ejemplo de código (es una parte del código que se muestra más adelante en el apartado 'Código de este ejemplo')

'Recuperar el valor del Binding
Dim nombreBE As BindingExpression = 
             textBoxNombreFichero.GetBindingExpression(TextBox.TextProperty)
'Enviar el valor al objeto enlazado y comprobar contenido con las reglas de negocio
nombreBE.UpdateSource()

Establecer las reglas de validación

Las reglas de validación son las reglas de negocio para ese dato, es decir, las condiciones que tiene que cumplir. Por ejemplo, para un nombre de fichero, las condiciones son, que exista una cadena, que la cadena sea un nombre de fichero valido, que exista en el disco (en realidad esta última condición es una condición especifica de este ejemplo.)

De alguna forma hay que decir al código Xaml, donde tiene que buscar esas reglas.

Existen dos formas de hacerlo. Las reglas estándar definidas por [ExceptionValidationRule], en este caso las condiciones de validación de datos deben estar en la propiedad enlazada, por ejemplo:

Ejemplo de reglas estándar

                    <Binding.ValidationRules>
                        <ExceptionValidationRule />
                    </Binding.ValidationRules>

En la clase que mapea este formulario y que se llama [DatosMapeadosFormulario] la propiedad [Directory] cuenta con reglas de validación estándar, de forma que su código Xaml y su código de propiedad son los que se muestran a continuación.

Private _directorio As String
Public Property Directorio() As String
    Get
        Return _directorio
    End Get
    Set(ByVal value As String)

        If value Is Nothing = True Then
            Throw New ArgumentException(
            "El nombre del directorio no puede ser una cadena nula (Nothing)")
        End If

        If value.Length = 0 = True Then
            Throw New ArgumentException(
            "El nombre del directorio no puede ser una cadena vacía (Empty)")
        End If

        If value.Trim.Length = 0 Then
            Throw New ArgumentException(
             "El nombre del directorio no puede ser una cadena de espacios")
        End If

        If _directorio <> value Then
            _directorio = value
        End If
    End Set
End Property

Existen problemas que este tipo de código no puede resolver, por ejemplo, comprobar una edad que este entre dos rangos numéricos que tiene que proporcionar el formulario. (Existe un ejemplo en la documentación MSDM).

Para resolver este problema se puede recurrir a la validación personalizada, a las reglas de validación personalizadas, que, evidentemente, se escriben en una clase por ejemplo En la clase [ValidacionNombreFicheroRules] (el código de esta clase se muestra más adelante, en el apartado "Codigo de este ejemplo").

Evidentemente hay que escribir en el código xaml que voy a usar esta clase para validar los datos de entrada y eso se hace de la siguiente manera:

                    <Binding.ValidationRules>
                        <!-- Las reglas de validación especificas de este dato -->
                        <namespaceLocal: ValidacionNombreFicheroRules />
                        <!-- Observa que no se emplean las reglas estándar 
                             (que están comentadas a continuación)-->
                        <!--<ExceptionValidationRule />-->
                    </Binding.ValidationRules>

El código de la clase esta (a idea) puesto después de este comentario, porque me interesa que leas la clase y luego te fijes en el código xaml inmediatamente anterior :-)

Observa que: La clase hereda directamente de [ValidationRule]. En la documentación de esta clase se dice lo siguiente:

Cuando se usa el modelo de enlace de datos de WPF, se puede asociar la propiedad ValidationRules al objeto de enlace.

Para crear reglas personalizadas, cree una subclase de esta clase e implemente el método Validate. Opcionalmente, utilice la clase ExceptionValidationRule integrada, que detecta las excepciones que se producen durante las actualizaciones de origen, o bien, la clase DataErrorValidationRule, que comprueba los errores generados por la implementación de IDataErrorInfo del objeto de origen.

El motor de enlaces comprueba cada clase ValidationRule asociada a un enlace cada vez que transfiere un valor de entrada, que es el valor de propiedad del destino de enlace, a la propiedad del origen de enlace.

Es decir que o bien se emplea la clase ExceptionValidationRule que es la opción integrada por defecto o bien escribo una regla personalizada escribiendo una clase que herede de ValidationRule y la uso en las reglas de Binding

Y la clase [ValidacionNombreFicheroRules] tendrá el código siguiente:

''' <summary>
'''   Wpf -  Implementa una regla de validación personalizada 
'''   para comprobar un nombre de fichero
''' </summary>
''' <remarks>
'''   ValidationRule (Clase)
'''   http://msdn.microsoft.com/es-es/library/ms617871(v=vs.100).aspx
'''</remarks>
Public Class ValidacionNombreFicheroRules
    Inherits ValidationRule


    ''' <summary>
    '''       Regla de validación personalizada
    ''' </summary>
    ''' <param name="value">El valor que se va a comprobar. En este caso un nombre de fichero</param>
    ''' <param name="cultureInfo">La cultura en uso</param>
    ''' <returns>Returns un objeto ValidationResult. </returns>
    ''' <remarks>
    '''     ValidationRule (Clase)
    '''     http://msdn.microsoft.com/es-es/library/ms617871(v=vs.100).aspx
    '''</remarks>
    Public Overrides Function Validate( _
                        value As Object,
                        cultureInfo As System.Globalization.CultureInfo) _
                    As System.Windows.Controls.ValidationResult

        If value Is Nothing Then
            Return New ValidationResult(False, "Problema - Objeto con valor Nothing")
        End If

        Try
            Dim auxNombre As String = Convert.ToString(value)

            If auxNombre Is Nothing = True Then
                Return New ValidationResult(
                           False, "El nombre de fichero no puede ser una cadena nula (Nothing)")
            End If

            If auxNombre.Length = 0 = True Then
                Return New ValidationResult(
                           False, "El nombre de fichero no puede ser una cadena vacia (Empty)")
            End If

            If auxNombre.Trim.Length = 0 Then
                Return New ValidationResult(
                           False, "El nombre de fichero no puede ser una cadena de espacios")
            End If

            '------------------------------------
            Dim objFileInfo As IO.FileInfo = New IO.FileInfo(auxNombre)

            If objFileInfo.Exists = False Then
                Using sw As New System.IO.StringWriter(cultureInfo)
                    sw.WriteLine("El fichero NO existe en el disco")
                    sw.WriteLine("Nombre completo del fichero:")
                    sw.WriteLine(" [{0}] ", auxNombre)
                    sw.Flush()
                    '
                    Return New ValidationResult(False, sw.ToString)
                End Using
            End If

            If objFileInfo.Length = 0 Then
                Using sw As New System.IO.StringWriter(cultureInfo)
                    sw.WriteLine("El fichero  [{0}]  SI existe en el disco pero esta vacio",
                                 objFileInfo.Name)
                    sw.WriteLine("Nombre completo del fichero:")
                    sw.WriteLine(" [{0}] ", objFileInfo.FullName)
                    sw.Flush()
                    '
                    Return New ValidationResult(False, sw.ToString)
                End Using
            End If


            '-----------------------------------------
            ' todo correcto
            ' devolver un valor de validator result
            Return New ValidationResult(True, Nothing)
            '
            '--------------------------------------------
        Catch ex As Exception
            Return New ValidationResult(
                     False,
                    "El nombre de fichero no tiene un formato correcto" &
                    Environment.NewLine &
                    ex.Message)
        End Try

    End Function

End Class


↑↑↑

El problema de los controles vacios

Problema. Por observación directa del comportamiento de este ejemplo, he visto que las reglas personalizadas no comprueban el caso de que el control quede vacio. Una situación de ejemplo. Tengo mi formulario de entrada con los dos campos vacios, hago clic en cada uno de ellos pero sin introducir nada, a continuación hago otro clic en el siguiente campo, sin introducir nada, y por ultimo uso el botón cancelar. En este caso en los controles del formulario no hay nada (están Empty) y el enlace de datos no funciona porque no se comprueba ni se detecta el caso de que los cuadros de texto (nombre de fichero y directorio) están vacios.


↑↑↑

Solución Uno.:

Escribir en el botón Aceptar código que compruebe y obligue a realizarse el enlace de datos y que compruebe si hay algún error y tome las medidas adecuadas al caso.

Ejemplo de código del botón Aceptar

Private Sub ButtonAceptar_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    ' recuperar el valor del Binding
    Dim nombreBE As BindingExpression = 
                    textBoxNombreFichero.GetBindingExpression(TextBox.TextProperty)

    ' enviar el valor al objeto mapeado y comprobar contenido con las reglas de negocio
    nombreBE.UpdateSource()

    ' recuperar el valor del Binding
    Dim directorioBE As BindingExpression = 
                     textBoxNombreDirectorio.GetBindingExpression(TextBox.TextProperty)

    ' enviar el valor al objeto mapeado y comprobar contenido con las reglas de negocio
    directorioBE.UpdateSource()


    If (nombreBE.HasError = True) OrElse (directorioBE.HasError = True) Then
        ' ha habido un error
        ' no hacer nada
        ' Fíjate que en este caso, NO cierro el formulario
    Else
        ' todo correcto
        ' recuperar el objeto enlazado
        ' cerrar el formulario
        ' --------------------------------------
        ' recuperar el objeto enlazado
        ' hay que hacerlo a través de un binding
        ' utilizo uno cualquiera de los binding al objeto (por ejemplo el primero)
        Dim objmapeadoBinding As Binding = 
            BindingOperations.GetBinding(textBoxNombreFichero, TextBox.TextProperty)

        ' hacer el casting
        _objMapeado = CType(objmapeadoBinding.Source, DatosMapeadosFormulario)

        ' cerrar el form
        Me.Close()
    End If

End Sub


↑↑↑

Solución Dos

Escribir en la clase que mapea el formulario La reglas de qué hacer si aparece un valor vacio o nulo. Independientemente de que también exista una regla de validación específica para ese campo. Es decir, en este caso se duplica el código siguiente (por ejemplo)

If value Is Nothing = True Then
     Throw New ArgumentNullException( 
           "El nombre del fichero no puede ser una cadena nula (Nothing)")
 End If

 If value.Length = 0 = True Then
     Throw New ArgumentException(
              "El nombre del fichero no puede ser una cadena vacía (Empty)")
 End If


↑↑↑

Solución Tres

Llamar específicamente a la clase que contiene las reglas de validación desde la propiedad de la clase que mapea los controles del formulario, algo así como

Private _nombreFichero As String
Public Property NombreFichero() As String
    Get
        Return _nombreFichero
    End Get
    Set(ByVal value As String)
        If _nombreFichero <> value Then
             
            ' Llamar específicamente a la clase que contiene las reglas de validación para este campo
            Dim interfaceValidacion As ValidationRule
            interfaceValidacion = New ValidacionNombreFicheroRules
            Call interfaceValidacion.Validate(value, System.Globalization.CultureInfo.CurrentCulture)

            ' si llega aquí no ha habido errores de validación
            ' actualizar el campo de la clase
            _nombreFichero = value


            ' Disparar el evento (Usando la funcion [OnPropertyChanged]) (Recomendada)
            ' ¡¡ Atención !! El nombre de la propiedad que devuelve
            '                la función de reflexión es [set_NombreFichero]
            Call OnPropertyChanged(System.Reflection.MethodBase.GetCurrentMethod.Name)
        End If

    End Set
End Property


↑↑↑

Problema que el formulario escuche los cambios de la clase enlazada

Para que el enlace de datos escuche las modificaciones de la clase enlazada, la clase tiene que implementar la interfaz [INotifyPropertyChanged]. Y además tenemos que conocer la referencia del objeto enlazado para poder modificarlo por código, porque, evidentemente, el formulario declara e instancia una clase que es la que utiliza.

Pero ese es otro problema que se resolverá a continuación


↑↑↑

El problema de recuperar la referencia al objeto enlazado

Existe otro problema que consiste en usar en el código el objeto enlazado (por partida doble) por un lado para poder cargar los datos y/o valores iniciales (si existen y si procede) y por otro para poder recuperar el objeto que contiene los datos que se han cargado a través de los controles del formulario.

Se podría pensar que realmente no hace falta porque si hay que proporcionar algún valor se hace referencia los controles del formulario y se cargan y/o se recuperan los valores, pero si en lugar de dos valores tenemos (por ejemplo 14) la cosa se complica un poquillo, y además, si ya existe esa clase con esa información para que me voy a dar mal haciendo cosas raras. Lo mejor es recuperar la referencia instancia de la clase y problema resuelto.

La forma de hacerlo es la siguiente:

En primer lugar se declara una variable interna del formulario (private) que contendrá una instancia del objeto enlazado (evidentemente no será la misma)

En segundo lugar en el evento Window.Loaded escribir el código que se muestra a continuación que lo que hace es recuperar el objeto enlazado a través del objeto Binding del primer Control TextBox enlazado. Las últimas líneas que escriben una frase en los controles del formulario sirven para comprobar que funcione y se pueden borrar.

Private _objMapeado As DatosMapeadosFormulario

Private Sub WindowPrueba_Loaded(
            sender As Object, e As System.Windows.RoutedEventArgs) 
            Handles Me.Loaded

    If _objMapeado Is Nothing Then
        ' --------------------------------------
        ' recuperar el objeto enlazado
        ' hay que hacerlo a través de un binding
        ' utilizo uno cualquiera de los binding al objeto (por ejemplo el primero)
        Dim objmapeadoBinding As Binding = 
            BindingOperations.GetBinding(textBoxNombreFichero, TextBox.TextProperty)
        ' hacer el casting
        _objMapeado = CType(objmapeadoBinding.Source, DatosMapeadosFormulario)
    End If

        ' comprobar el funcionamiento
    _objMapeado.NombreFichero = "Esto debe verse"
    _objMapeado.Directorio =    "Esto también debe verse"

End Sub

Al poner en marcha el programa en la ventana principal aparece lo siguiente:

Imagen 05

Imagen 05 - Despues de haber cargado los datos del objeto enlazado

Por último, lo único que tengo que hacer es usar una propiedad para escribir/recuperar el objeto del formulario

Public Property FormularioDatosMapeados() As DatosMapeadosFormulario
     Get
         Return _objMapeado
     End Get
     Set(ByVal value As DatosMapeadosFormulario)
         _objMapeado = value
     End Set
 End Property


↑↑↑

Los Botones Aceptar/Cancelar

Los clásicos botones aceptar cancelar se pueden definir de la siguiente manera

Imagen 04 - Botones Aceptar / Cancelar

Imagen 04 - Botones Aceptar / Cancelar

       <!-- 
         ...................................................
         Botones de aceptar y cancelar 
         ...................................................
         Un cuadro de diálogo proporciona normalmente un botón 
         especial para cancelar un diálogo, el botón cuya  
         propiedad de IsCancel se establece en true.
        
         Un botón configurado de esta manera cerrará automáticamente 
         una ventana cuando se presiona, o cuando se presiona 
         la tecla ESC. 
         En cualquiera de estos casos, DialogResult permanece false.
 
         Un cuadro de diálogo también proporciona normalmente 
         un botón aceptar, que es el botón cuya propiedad 
         de IsDefault se establece en true.
         Un botón configurado de esta manera provocará 
         el evento de Click cuando éste o la tecla ENTRAR se presiona.
        -->

        <Grid  Name="GridPanelBotonesOkCancel" 
               HorizontalAlignment="Right"  
               VerticalAlignment="Bottom" 
               Margin="0,0,12,12">
            <Grid.ColumnDefinitions>
                <ColumnDefinition x:Uid="ColumnDefinition1" 
                                  SharedSizeGroup="Buttons" Width="Auto" />
                <ColumnDefinition x:Uid="ColumnDefinition2" 
                                  SharedSizeGroup="Buttons" Width="Auto" />
            </Grid.ColumnDefinitions>
            <Button Width="80" Content="Aceptar"  Name="ButtonAceptar"    Grid.Column="0"
                    Click="ButtonAceptar_Click"  
                    IsDefault="True" />
            <Button Width="80" Content="Cancelar" Name="ButtonCancelar"   Grid.Column="1"
                    Click="ButtonCancelar_Click" 
                    IsCancel="True" />
        </Grid>


↑↑↑

El código completo de este ejemplo


↑↑↑

Window - Código xaml

<Window x:Class="WindowPrueba"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:namespaceLocal="clr-namespace:BindValidation_VB"
    Title="WindowPrueba" Height="232" Width="339">


    <Window.Resources>
        <namespaceLocal:DatosMapeadosFormulario  x:Key="IntanciaDatosMapeadosFormulario"/>


        <!-- Estilos que se aplican cuando hay un error  -->
        <Style x:Key="estilosParaErrorDelTextBox" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                            Path=(Validation.Errors)[0].ErrorContent}"/>

                    <Setter Property="Background" Value="Yellow" />
                    <Setter Property="BorderBrush" Value="Red" />

                </Trigger>
            </Style.Triggers>
        </Style>

        <!-- Plantilla que simula un error provider -->
        <ControlTemplate x:Key="templateSimularUnFormErrorProvider">
            <DockPanel>
                <!--<Label  Foreground="Red" Background="Azure"  Content="(!)" />-->
                <TextBlock Foreground="Red" FontSize="20"> (!)</TextBlock>
                <AdornedElementPlaceholder />
            </DockPanel>
        </ControlTemplate>

    </Window.Resources>



    <Grid>

        <Label Height="28" HorizontalAlignment="Left" Margin="36,24,0,0" VerticalAlignment="Top" 
               Content="Introduce un nombre completo de fichero"  
               Name="LabelTituloNombreFichero"  />

        <TextBox Name="textBoxNombreFichero" FontSize="15" Margin="40,51,65,97"   
                  Style="{StaticResource  estilosParaErrorDelTextBox}"  
                  Validation.ErrorTemplate="{StaticResource templateSimularUnFormErrorProvider}" 
                  VerticalAlignment="Top">

            <TextBox.Text>
                <Binding Source="{StaticResource IntanciaDatosMapeadosFormulario}"
                         Path="NombreFichero" 
                         UpdateSourceTrigger="LostFocus">
                    <Binding.ValidationRules>
                        <!-- Las reglas de validacion especificas de este dato -->
                        <namespaceLocal:ValidacionNombreFicheroRules />
                        <!-- Observa que no se emplean las reglas estándar 
                             (que están comentadas a continuación)-->
                        <!--<ExceptionValidationRule />-->
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>


        <Label Height="28" HorizontalAlignment="Left" Margin="40,83,0,0" VerticalAlignment="Top" 
               Content="Introduce un Directorio" 
               Name="LabelTituloDirectorio"  />

        <TextBox Name="textBoxNombreDirectorio" FontSize="15" Margin="40,111,65,0"   
                  Style="{StaticResource  estilosParaErrorDelTextBox}"  
                 Validation.ErrorTemplate="{StaticResource templateSimularUnFormErrorProvider}" 
                 VerticalAlignment="Top">

            <TextBox.Text>
                <Binding Source="{StaticResource IntanciaDatosMapeadosFormulario}"
                         Path="Directorio" 
                         UpdateSourceTrigger="LostFocus" 
                         NotifyOnValidationError="True"  
                         TargetNullValue="Introduzca aquí un directorio">
                    <Binding.ValidationRules>
                        <ExceptionValidationRule />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>


        <!-- 
         ...................................................
         Botones de aceptar y cancelar 
         ...................................................
         Un cuadro de diálogo proporciona normalmente un botón 
         especial para cancelar un diálogo, el botón cuya  
         propiedad de IsCancel se establece en true.
        
         Un botón configurado de esta manera cerrará automáticamente 
         una ventana cuando se presiona, o cuando se presiona 
         la tecla ESC. 
         En cualquiera de estos casos, DialogResult permanece false.
 
         Un cuadro de diálogo también proporciona normalmente 
         un botón aceptar, que es el botón cuya propiedad 
         de IsDefault se establece en true.
         Un botón configurado de esta manera provocará 
         el evento de Click cuando éste o la tecla ENTRAR se presiona.
        -->

        <Grid  Name="GridPanelBotonesOkCancel" 
               HorizontalAlignment="Right"  
               VerticalAlignment="Bottom" 
               Margin="0,0,12,12">
            <Grid.ColumnDefinitions>
                <ColumnDefinition x:Uid="ColumnDefinition1" SharedSizeGroup="Buttons" Width="Auto" />
                <ColumnDefinition x:Uid="ColumnDefinition2" SharedSizeGroup="Buttons" Width="Auto" />
            </Grid.ColumnDefinitions>
            <Button Width="80" Content="Aceptar"  Name="ButtonAceptar"    Grid.Column="0"
                    Click="ButtonAceptar_Click"  
                    IsDefault="True" />
            <Button Width="80" Content="Cancelar" Name="ButtonCancelar"   Grid.Column="1"
                    Click="ButtonCancelar_Click" 
                    IsCancel="True" />
        </Grid>


    </Grid>
</Window>


↑↑↑

Window - Código Visual Basic

Public Class WindowPrueba


    Private _objMapeado As DatosMapeadosFormulario


    Private Sub WindowPrueba_Loaded(sender As Object, 
                                    e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        If _objMapeado Is Nothing Then
            ' --------------------------------------
            ' recuperar el objeto enlazado
            ' hay que hacerlo a traves de un binding
            ' utilizo uno cualquiera de los binding al objeto (por ejemplo el primero)
            Dim objmapeadoBinding As Binding = 
                   BindingOperations.GetBinding(textBoxNombreFichero, TextBox.TextProperty)
            ' hacer el casting
            _objMapeado = CType(objmapeadoBinding.Source, DatosMapeadosFormulario)
        End If

        _objMapeado.NombreFichero = "Esto debe verse"
        _objMapeado.Directorio = "Esto tambien debe verse"

    End Sub


    Public Property FormularioDatosMapeados() As DatosMapeadosFormulario
        Get
            Return _objMapeado
        End Get
        Set(ByVal value As DatosMapeadosFormulario)
            _objMapeado = value
        End Set
    End Property


    ' Un cuadro de diálogo proporciona normalmente un botón especial para cancelar un diálogo,
    'el botón cuya propiedad de IsCancel se establece en true.

    'Un botón configuró esta manera cerrará automáticamente una ventana cuando o se presiona, 
    'o cuando se presiona la tecla ESC.En cualquiera de estos casos, DialogResult permanece false.

    'Un cuadro de diálogo también proporciona normalmente un botón aceptar, que es el botón 
    'cuya propiedad de IsDefault se establece en true.Un botón configuró esta manera provocará 
    'el evento de Click cuando éste o la tecla ENTRAR se presiona.

    'Sin embargo, automáticamente no se cerrará el cuadro de diálogo, 
    'ni establecerá DialogResult a true.Debe escribir manualmente este código, 
    'normalmente del controlador de eventos de Click para el botón predeterminado.      


    Private Sub ButtonAceptar_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
        ' Recuperar el valor del Binding
        Dim nombreBE As BindingExpression = 
                     textBoxNombreFichero.GetBindingExpression(TextBox.TextProperty)
        ' Enviar el valor al objeto mapeado y comprobar contenido con las reglas de negocio
        nombreBE.UpdateSource()

        ' recuperar el valor del Binding
        Dim directorioBE As BindingExpression = textBoxNombreDirectorio.GetBindingExpression(TextBox.TextProperty)
        ' enviar el valor al objeto mapeado y comprobar contenido con las reglas de negocio
        directorioBE.UpdateSource()


        If (nombreBE.HasError = True) OrElse (directorioBE.HasError = True) Then
            ' ha haido un error
            ' no hacer nada
        Else
            ' todo correcto
            ' recuperar el objeto enlazado
            ' cerrar el formulario
            ' --------------------------------------
            ' recuperar el objeto enlazado
            ' hay que hacerlo a traves de un binding
            ' utilizo uno cualquiera de los binding al objeto (por ejemplo el primero)
            Dim objmapeadoBinding As Binding = BindingOperations.GetBinding(textBoxNombreFichero, TextBox.TextProperty)
            ' hacer el casting
            _objMapeado = CType(objmapeadoBinding.Source, DatosMapeadosFormulario)

            ' cerrar el form
            'DialogResult = True
            Me.Close()
        End If

    End Sub

    Private Sub ButtonCancelar_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
        ' cerrar el form
        'DialogResult = False
        Me.Close()
    End Sub

End Class


↑↑↑

Clase [ValidacionNombreFicheroRules]

''' <summary>
'''   Wpf -  Implementa una regla de validacion
'''   personalizada para comprobar un nombre de fichero
''' </summary>
''' <remarks>
'''   ValidationRule (Clase)
'''   http://msdn.microsoft.com/es-es/library/ms617871(v=vs.100).aspx
'''</remarks>
Public Class ValidacionNombreFicheroRules
    Inherits ValidationRule


    ''' <summary>
    '''       Regla de validacion personalizada
    ''' </summary>
    ''' <param name="value">El valor que se va a comprobar. En este caso un nombre de fichero</param>
    ''' <param name="cultureInfo">La cultura en uso</param>
    ''' <returns>Returns un objeto ValidationResult. </returns>
    ''' <remarks>
    '''     ValidationRule (Clase)
    '''     http://msdn.microsoft.com/es-es/library/ms617871(v=vs.100).aspx
    '''</remarks>
    Public Overrides Function Validate( _
                        value As Object,
                        cultureInfo As System.Globalization.CultureInfo) _
                    As System.Windows.Controls.ValidationResult

        If value Is Nothing Then
            Return New ValidationResult(False, "Problema - Objeto con valor Nothing")
        End If

        Try
            Dim auxNombre As String = Convert.ToString(value)

            If auxNombre Is Nothing = True Then
                Return New ValidationResult(
                       False, "El nombre de fichero no puede ser una cadena nula (Nothing)")
            End If

            If auxNombre.Length = 0 = True Then
                Return New ValidationResult(
                       False, "El nombre de fichero no puede ser una cadena vacía (Empty)")
            End If

            If auxNombre.Trim.Length = 0 Then
                Return New ValidationResult(
                       False, "El nombre de fichero no puede ser una cadena de espacios")
            End If

            '------------------------------------
            Dim objFileInfo As IO.FileInfo = New IO.FileInfo(auxNombre)

            If objFileInfo.Exists = False Then
                Using sw As New System.IO.StringWriter(cultureInfo)
                    sw.WriteLine("El fichero NO existe en el disco")
                    sw.WriteLine("Nombre completo del fichero:")
                    sw.WriteLine(" [{0}] ", auxNombre)
                    sw.Flush()
                    '
                    Return New ValidationResult(False, sw.ToString)
                End Using
            End If

            If objFileInfo.Length = 0 Then
                Using sw As New System.IO.StringWriter(cultureInfo)
                    sw.WriteLine("El fichero  [{0}]  SI existe en el disco pero esta vacio",
                                 objFileInfo.Name)
                    sw.WriteLine("Nombre completo del fichero:")
                    sw.WriteLine(" [{0}] ", objFileInfo.FullName)
                    sw.Flush()
                    '
                    Return New ValidationResult(False, sw.ToString)
                End Using
            End If


            '-----------------------------------------
            ' todo correcto
            ' devolver un valor de validator result
            Return New ValidationResult(True, Nothing)
            '
            '--------------------------------------------
        Catch ex As Exception
            Return New ValidationResult(
                     False,
                    "El nombre de fichero no tiene un formato correcto" &
                    Environment.NewLine &
                    ex.Message)
        End Try

    End Function
End Class


↑↑↑

Clase [DatosMapeadosFormulario]

Public Class DatosMapeadosFormulario
    Implements System.ComponentModel.INotifyPropertyChanged

    'Una clase de datos personalizada POCO debe tener un constructor 
    'public o protected sin parámetros.
    'Utilice un constructor protected sin parámetros si desea utilizar el 
    'método CreateObject para crear un proxy para la entidad POCO.
    'Al llamar al método CreateObject, no se garantiza la creación del 
    'proxy: la clase POCO debe cumplir los otros requisitos que se describen en este tema.

    ' Para detectar los cambios en el origen (aplicables a los enlaces OneWay y TwoWay), 
    ' el origen debe implementar un mecanismo apropiado de notificación de cambios de  
    ' propiedades, como INotifyPropertyChanged.  



#Region "Evento PropertyChanged [Versión 2011-12-21]"

    '-----------------------------------------------------------------------
    ' Declaración del evento usando un EventHandler genérico (Recomendada)
    ' !! Observación !!! Se produce un error al serializar eventos Genéricos
    Public Event PropertyChanged As  _
              System.ComponentModel.PropertyChangedEventHandler _
              Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

    '----------------------------------------------------------------------
    ''' <summary>Funcion que dispara el evento [PropertyChanged]</summary>
    ''' <param name="nombreDeLaPropiedadChanged">
    '''    Una cadena con el nombre de la propiedad que ha cambiado
    ''' </param>
    ''' <remarks>
    ''' <code> http://msdn.microsoft.com/es-es/library/
    '''        system.componentmodel.inotifypropertychanged(VS.95).aspx
    ''' </code>
    '''</remarks>
    Private Sub OnPropertyChanged(ByVal nombreDeLaPropiedadChanged As String)
        ' Evitar problemas tontos
        If String.IsNullOrEmpty(nombreDeLaPropiedadChanged) = True Then Exit Sub

        ' Quitar el prefijo Get o Set si lo lleva
        If nombreDeLaPropiedadChanged.ToUpper.StartsWith("set_".ToUpper) = True OrElse
           nombreDeLaPropiedadChanged.ToUpper.StartsWith("get_".ToUpper) = True Then
            nombreDeLaPropiedadChanged = nombreDeLaPropiedadChanged.Remove(0, 4)
        End If


        ' Disparar el evento
        RaiseEvent PropertyChanged( _
                  Me, New System.ComponentModel.PropertyChangedEventArgs(nombreDeLaPropiedadChanged))
    End Sub

#End Region



    Public Sub New()
        ' no hacer nada
    End Sub

    Public Function CreateObjetc() As DatosMapeadosFormulario
        Return Me
    End Function


    Private _nombreFichero As String
    Public Property NombreFichero() As String
        Get
            Return _nombreFichero
        End Get
        Set(ByVal value As String)
            If _nombreFichero <> value Then
                _nombreFichero = value

                ' Disparar el evento (Usando la función [OnPropertyChanged]) (Recomendada)
                ' ¡¡ Atención !! El nombre de la propiedad que devuelve
                '                la función de reflexión es [set_NombreFichero]
                Call OnPropertyChanged(System.Reflection.MethodBase.GetCurrentMethod.Name)
            End If

        End Set
    End Property



    Private _directorio As String
    Public Property Directorio() As String
        Get
            Return _directorio
        End Get
        Set(ByVal value As String)

            If value Is Nothing = True Then
                Throw New ArgumentNullException(
                       "El nombre del directorio no puede ser una cadena nula (Nothing)")
            End If

            If value.Length = 0 = True Then
                Throw New ArgumentException(
                      "El nombre del directorio no puede ser una cadena vacia (Empty)")
            End If

            If value.Trim.Length = 0 Then
                Throw New ArgumentException(
                     "El nombre del directorio no puede ser una cadena de espacios")
            End If

            If _directorio <> value Then
                _directorio = value

                ' Disparar el evento (Usando la función [OnPropertyChanged]) (Recomendada)
                ' ¡¡ Atención !! El nombre de la propiedad que devuelve
                '                la función de reflexión es [set_Directorio]
                Call OnPropertyChanged(System.Reflection.MethodBase.GetCurrentMethod.Name)
            End If
        End Set
    End Property

End Class


↑↑↑

Referencia Bibliográfica


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]