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
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 - Aspecto inicial del formulario
Nuestra clase que mapea este formulario y que se llama [DatosMapeadosFormulario] es la siguiente:
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'
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 - 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>
Para enlazar esa clase hay que dar varios pasos,
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:
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:
<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:
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}"
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}"
<Binding Source="{StaticResource IntanciaDatosMapeadosFormulario}" Path="NombreFichero" UpdateSourceTrigger="LostFocus">
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
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()
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
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.
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
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
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
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
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 - 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
#Region "Actualizar los valores del combo" '----------------------------------------------------------------- ' ¡¡¡ ATENCION !!! ' este código usa un objeto vista llamado[DatosPrestamoVO] ' y muestra como obtener la instancia de un objeto enlazado ' a los controles de una ventana. ' También muestra como usar un evento de un ComboBox para actualizar ' el objeto[Vista] a través de la instancia del código ' Por ultimo esta escrito a prueba de errores que ocurren cuando ' alguno de los controles de la ventana, aun no se han instanciado '(porque no les dio tiempo) '----------------------------------------------------------------- ' Objeto que tendrá la instancia de la clase[Vista] usada en el ' enlace de datos de los controles de la ventana Private _objMapeado As DatosPrestamoVO ''' <summary> '''[ReadOnly] Propiedad que proporciona la instancia de la clase[Vista] ''' </summary> ''' <value> ''' Una instancia de la clase[Vista] enlazada a los controles de la ventana ''' </value> Public ReadOnly Property ObjetoDatosPrestamoVOEnlazado()As DatosPrestamoVO Get If _objMapeado Is Nothing Then Call EnlazarDatosPrestamoVO() End If Return _objMapeado End Get End Property '------------------------------------------------------------- ' Se ocupa de realizar el enlace del objeto[vista] enlazada ' con los controles de la ventana ' y que permitirá usarla a través del código Private Sub EnlazarDatosPrestamoVO() ' solo funciona si el objeto De la clase es NULL If _objMapeado Is Nothing Then '------------------------------------------------------------------ ' Ademas, el control que usamos para recuperar el objeto enlazado ' tiene que estar definido en la ventana(no puede ser NULL) ' La razón es que cuando se instancia la ventana, podemos llamar '(seguro que ocurre) al algún control que aun no se haya instanciado. ' De esta forma espero y no disparo ningún error, por objeto NULL ' y la próxima vez que se llame esta función ' el control ya estará instanciado y entonces funcionara '------------------------------------------------------------------ If TextBoxCapital Is Nothing Then Exit Sub ' -------------------------------------- ' 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(TextBoxCapital,TextBox.TextProperty) ' Hacer el casting _objMapeado = CType(objmapeadoBinding.Source,DatosPrestamoVO) End If ' comprobar el funcionamiento _objMapeado.PrecioDeLaVivienda = 200.22D End Sub ' -------------------------------------------------------- ' Situación del ejemplo ' Estoy usando un combo box extendido que ha cargado(y muestra) una enumeración ' este combo NO esta enlazado con la clase[Vista] que contiene los datos ' pero me interesa actualizar el valor de la clase[Vista] porque es importante ' por eso uso el evento del combo[SelectionChanged], para saber que ha cambiado ' el valor del combo y que tengo que actualizarlo en la clase[Vista] ' para ello utilizo el Objeto[_objMapeado] que previamente he calculado ' ' Se dispara cuando cambia el valor del ComboBox ' Se actualiza el valor en[DatosPrestamoVO] Private Sub MyComboBoxFormaDeHacerLosPagos_SelectionChanged(sender As Object,e As SelectionChangedEventArgs) ' Si el objeto[_objMapeado] enlazado con la clase[Vista] no esta instanciado ' entonces instanciarlo ' Puede ocurrir que el proceso falle(así esta programado) ' porque algún control de la ventana, aun no le haya dado tiempo a estar instanciado, ' por eso me aseguro(Preguntado una segunda vez) que[_objMapeado] no tenga un valor NULL ' Si todo es correcto se actualiza la clase[Vista] con el valor del Combo If _objMapeado Is Nothing Then EnlazarDatosPrestamoVO() If _objMapeado Is Nothing Then Exit Sub End If End If ' Actualizar el valor de la case[Vista] _objMapeado.FormaPagoPrestamo = MyComboBoxFormaDeHacerLosPagos.ZValorToCampoEnumerado End Sub #End Region
Los clásicos botones aceptar cancelar se pueden definir de la siguiente manera
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>
<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>
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
''' <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
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