Para optimizar el enlace de datos en WPF y garantizar una arquitectura altamente escalable, es fundamental aplicar los principios de separación de responsabilidades, pattrones de diseño flexibles y una estructura modular. La implementación de enumeradores para tipos de control, interfaces de validación y plantillas dinámicas permite agregar nuevas funcionalidades sin alterar el código existente.
Definición de Tipos de Control
En lugar de usar cadenas o valores mágicos para determinar el control de interfaz, se introduce un enumerador que facilita la extensión a nuevos componentes visuales como selectores de fecha o casillas de verificación.
public enum TipoControl
{
CajaTexto,
ListaDesplegable,
CasillaVerificacion,
SelectorFecha
}
Modelo de Datos Puro
Se aísla la estructura de datos en una clase simple, desacoplada de la lógica de presentación.
public class AjusteConfiguracion
{
public int Identificador { get; set; }
public string Etiqueta { get; set; }
public string Dato { get; set; }
public string Medida { get; set; }
public string Detalle { get; set; }
public string LimiteInferior { get; set; }
public string LimiteSuperior { get; set; }
public List<string> Alternativas { get; set; }
public TipoControl Tipo { get; set; }
public bool Activo { get; set; } = true;
}
Sistema de Validación Extensible
Mediante una interfaz de validación, se puede inyectar cualquier regla de negocio sin modificar el ViewModel principal.
public interface IValidadorDato
{
bool EsValido(AjusteConfiguracion ajuste, out string mensajeError);
}
public class ValidadorRango : IValidadorDato
{
public bool EsValido(AjusteConfiguracion ajuste, out string mensajeError)
{
mensajeError = string.Empty;
if (ajuste.Tipo != TipoControl.CajaTexto) return true;
if (string.IsNullOrWhiteSpace(ajuste.Dato) || string.IsNullOrWhiteSpace(ajuste.LimiteInferior) || string.IsNullOrWhiteSpace(ajuste.LimiteSuperior))
return true;
if (double.TryParse(ajuste.Dato, out double val) &&
double.TryParse(ajuste.LimiteInferior, out double min) &&
double.TryParse(ajuste.LimiteSuperior, out double max))
{
if (val < min)
{
mensajeError = "El valor está por debajo del mínimo permitido";
return false;
}
if (val > max)
{
mensajeError = "El valor supera el máximo permitido";
return false;
}
}
return true;
}
}
public class ValidadorOpciones : IValidadorDato
{
public bool EsValido(AjusteConfiguracion ajuste, out string mensajeError)
{
mensajeError = string.Empty;
if (ajuste.Tipo != TipoControl.ListaDesplegable) return true;
if (!string.IsNullOrEmpty(ajuste.Dato) && ajuste.Alternativas != null && !ajuste.Alternativas.Contains(ajuste.Dato))
{
mensajeError = "El valor no se encuentra en la lista de opciones";
return false;
}
return true;
}
}
ViewModel con Notificación de Errores
El ViewModel gestiona la interacción de la interfaz y notifica los errores de validación a través de INotifyDataErrorInfo.
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
public class ConfiguracionViewModel : BindableBase, INotifyDataErrorInfo
{
private readonly AjusteConfiguracion _ajuste;
private readonly IValidadorDato _validador;
private readonly Dictionary<string, List<string>> _errores = new();
public ConfiguracionViewModel(AjusteConfiguracion ajuste, IValidadorDato validador)
{
_ajuste = ajuste;
_validador = validador;
}
public int Identificador
{
get => _ajuste.Identificador;
set => SetProperty(ref _ajuste.Identificador, value);
}
public string Etiqueta
{
get => _ajuste.Etiqueta;
set => SetProperty(ref _ajuste.Etiqueta, value);
}
public string Dato
{
get => _ajuste.Dato;
set
{
SetProperty(ref _ajuste.Dato, value, VerificarDato);
RaisePropertyChanged(nameof(DatoFormateado));
}
}
public string DatoFormateado => _ajuste.Tipo == TipoControl.CajaTexto && !string.IsNullOrEmpty(_ajuste.Medida)
? $"{_ajuste.Dato} {_ajuste.Medida}"
: _ajuste.Dato;
public string Medida
{
get => _ajuste.Medida;
set => SetProperty(ref _ajuste.Medida, value);
}
public string Detalle
{
get => _ajuste.Detalle;
set => SetProperty(ref _ajuste.Detalle, value);
}
public string LimiteInferior
{
get => _ajuste.LimiteInferior;
set => SetProperty(ref _ajuste.LimiteInferior, value, VerificarDato);
}
public string LimiteSuperior
{
get => _ajuste.LimiteSuperior;
set => SetProperty(ref _ajuste.LimiteSuperior, value, VerificarDato);
}
public ObservableCollection<string> Alternativas
{
get => new ObservableCollection<string>(_ajuste.Alternativas ?? new List<string>());
set => SetProperty(ref _ajuste.Alternativas, value.ToList());
}
public TipoControl Tipo
{
get => _ajuste.Tipo;
set => SetProperty(ref _ajuste.Tipo, value);
}
public bool Activo
{
get => _ajuste.Activo;
set => SetProperty(ref _ajuste.Activo, value);
}
private void VerificarDato()
{
_errores.Remove(nameof(Dato));
if (!_validador.EsValido(_ajuste, out string mensajeError))
{
_errores[nameof(Dato)] = new List<string> { mensajeError };
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Dato)));
}
public bool HasErrors => _errores.Any();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string nombrePropiedad) => _errores.TryGetValue(nombrePropiedad, out var errs) ? errs : null;
}
Gestor de Colecciones
El contenedor principal administra el listado de elementos y expone comandos para manipular la colección dinámicamente.
using Prism.Commands;
using Prism.Mvvm;
using System.Collections.ObjectModel;
public class ProyectoViewModel : BindableBase
{
private ObservableCollection<ConfiguracionViewModel> _listaAjustes;
public ObservableCollection<ConfiguracionViewModel> ListaAjustes
{
get => _listaAjustes;
set => SetProperty(ref _listaAjustes, value);
}
public DelegateCommand AgregarAjusteCommand { get; }
public ProyectoViewModel()
{
ListaAjustes = new ObservableCollection<ConfiguracionViewModel>
{
new ConfiguracionViewModel(new AjusteConfiguracion
{
Identificador = 1, Etiqueta = "Tolerancia", Dato = "0.1", LimiteInferior = "0.1", LimiteSuperior = "1", Medida = "°C",
Detalle = "Margen de estabilidad térmica", Tipo = TipoControl.CajaTexto
}, new ValidadorRango()),
new ConfiguracionViewModel(new AjusteConfiguracion
{
Identificador = 2, Etiqueta = "ModoConmutacion", Dato = "Arriba", Detalle = "Dirección de conmutación",
Alternativas = new List<string> { "Arriba", "Abajo", "Arriba-Abajo", "Abajo-Arriba" }, Tipo = TipoControl.ListaDesplegable
}, new ValidadorOpciones())
};
AgregarAjusteCommand = new DelegateCommand(AgregarAjuste);
}
private void AgregarAjuste()
{
ListaAjustes.Add(new ConfiguracionViewModel(new AjusteConfiguracion
{
Identificador = ListaAjustes.Count + 1,
Etiqueta = "Nuevo Parámetro",
Tipo = TipoControl.CajaTexto
}, new ValidadorRango()));
}
}
Convertidores de Interfaz
Se implementan convertidores genéricos para controlar la visibilidad según el tipo de control y aplicar estilos visuales basados en el estado de validación.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
public class ConvertidorVisibilidadControl : IValueConverter
{
public object Convert(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
if (valor is TipoControl tipo && parametro is string tipoEsperado &&
Enum.TryParse(tipoEsperado, out TipoControl objetivo))
{
return tipo == objetivo ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
throw new NotImplementedException();
}
}
public class ConvertidorColorValidacion : IValueConverter
{
public object Convert(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
if (valor is IEnumerable<string> errores && errores.Any())
return System.Windows.Media.Brushes.Red;
return System.Windows.Media.Brushes.Black;
}
public object ConvertBack(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
throw new NotImplementedException();
}
}
public class ConvertidorBooleano : IValueConverter
{
public object Convert(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
return valor?.ToString().ToLower() == "true";
}
public object ConvertBack(object valor, Type tipoObjetivo, object parametro, CultureInfo cultura)
{
return valor is bool val ? val.ToString() : "False";
}
}
Implementación XAML Dinámica
El XAML aprovecha ContentPresenter y plantillas de datos para renderizar controles de forma dinámica según el tipo definido en el ViewModel.
<Window x:Class="TuEspacioNombres.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TuEspacioNombres"
Title="Ventana Principal" Height="450" Width="800">
<Window.Resources>
<local:ConvertidorVisibilidadControl x:Key="VisibilidadControlConv"/>
<local:ConvertidorColorValidacion x:Key="ColorValidacionConv"/>
<local:ConvertidorBooleano x:Key="BooleanoConv"/>
<DataTemplate x:Key="PlantillaTexto">
<TextBox Text="{Binding Dato, UpdateSourceTrigger=PropertyChanged, Delay=1000, ValidatesOnNotifyDataErrors=True}"
Width="100" Height="30" HorizontalContentAlignment="Center" FontWeight="Bold">
<TextBox.Foreground>
<Binding Path="Errors[Dato]" Converter="{StaticResource ColorValidacionConv}"/>
</TextBox.Foreground>
</TextBox>
</DataTemplate>
<DataTemplate x:Key="PlantillaCombo">
<ComboBox ItemsSource="{Binding Alternativas}"
SelectedItem="{Binding Dato, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
Width="100" Height="30" HorizontalContentAlignment="Center"/>
</DataTemplate>
<DataTemplate x:Key="PlantillaCheck">
<CheckBox IsChecked="{Binding Dato, Converter={StaticResource BooleanoConv}, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding ModeloProyecto.ListaAjustes}" Margin="4" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Width="60" Binding="{Binding Identificador}" IsReadOnly="True" Foreground="Gray"/>
<DataGridTemplateColumn Header="Ajuste" Width="200">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Etiqueta}" VerticalAlignment="Center" Width="180" FontWeight="Bold" Foreground="DarkBlue"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Valor" Width="120">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource PlantillaTexto}"
Visibility="{Binding Tipo, Converter={StaticResource VisibilidadControlConv}, ConverterParameter=CajaTexto}"/>
<ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource PlantillaCombo}"
Visibility="{Binding Tipo, Converter={StaticResource VisibilidadControlConv}, ConverterParameter=ListaDesplegable}"/>
<ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource PlantillaCheck}"
Visibility="{Binding Tipo, Converter={StaticResource VisibilidadControlConv}, ConverterParameter=CasillaVerificacion}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Unidad" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Label Content="{Binding Medida}" VerticalAlignment="Center" Width="80" HorizontalContentAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Mínimo" Width="110">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding LimiteInferior, UpdateSourceTrigger=PropertyChanged, Delay=500}"
Width="100" Height="30" HorizontalContentAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Máximo" Width="110">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding LimiteSuperior, UpdateSourceTrigger=PropertyChanged, Delay=500}"
Width="100" Height="30" HorizontalContentAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Notas" Width="380">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Detalle, UpdateSourceTrigger=PropertyChanged, Delay=500}"
Foreground="Gray" IsReadOnly="True" Width="340" Height="30" HorizontalContentAlignment="Left"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Añadir Ajuste" Command="{Binding ModeloProyecto.AgregarAjusteCommand}" Width="100" Height="30" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Bottom"/>
</Grid>
</Window>