Hace unas semanas, entramos en contacto con el primero de
nuestros Repositorios
Genéricos de Entity Framework, los de tipo desconectado. En esta segunda
entrega vamos a abordar el segundo tipo disponible por características y uso,
este es otro el llamado tipo conectado.
En esta nueva clase de repositorio genérico, aparece una actor fundamental que no es otro
que ObservableCollection<T>,
amigo inseparable de nuestro tipo conectado.
Índice
- Entity Framework Repositorio Genérico Conectado.
- El método Set<TEntity> DbContext.
- Clases de ejemplo.
- Construyendo un Entity Framework Repositorio Genérico Conectado.
- All / AllAsync
- Find / FindAsync
- GetData / GetDataAsync
- SaveChanges / SaveChangesAsync
- HasChanges / HasChangesAsync
- Extrayendo la Interface.
- WPF Ejemplo.
- Extendiendo ConGeneriRepository<TEntitiy>.
- Proyecto de prueba.
Entity Framework Repositorio Genérico
Conectado
Entity Framework Repositorio Genérico Conectado es usado por los llamados procesos conectados. Estos procesos están formados fundamentalmente por aplicaciones de tipo WPF, Silverlight, Windows Forms, console , etc.
Estos repositorios trabajan con grupos de cambios y
normalmente están enlazados a objetos visuales de tipo ItemsControls (Datagrids,
Listbox, Listviews, etc). El tipo de repositorio genérico conectado trabaja
directamente con la manipulación de estos objetos vinculados directamente con
el ObservableCollection<T>.
Sus
principales carácterísticas son:
- Debe de recibir un objeto DbContext mediante inyección de dependencias.
- Debe implementar IDisposable interface para poder liberar los recursos no administrados.
- Debe de tener una propiedad protected de tipo DbContext. Esta propieda vivirá durante toda la vida del repositorio y solo se destruirá en el método Dispose.
- La propiedad DbContext escuchará todos los cambios que se producen.
- Al contrario que con el repositorio desconectado este no tendrá los métodos: add, remove y update, ya que estas acciones se producen mediante iteraciones directas contra la propiedad Local del DbSet. Local es una propiedad de tipo ObservableCollection<TEntity> (INotifyCollectionChanged) y esta normalmente suele ser vinculada a objetos de tipo: ListBox, ListView, DataGrid, etc.
Entity Framework generic repository connected is used in state process as WPF, Silverlight,
Windows Forms, console app, etc.
This
repository working with group changes, and it has fixed to ItemsControls ( Datagrids, ListBox, ListViews, etc. ).
This generic repository type works with
direct DataGrid modifications, linked
directly with ObservableCollection<T>.
Its main
characteristics are:
- Should receive the DbContext from dependency injection.
- Should implements IDisposable interface for releasing unmanaged resources.
- Should has a DbContext protected property. This property will be alive during generic repository life and will only die in the Dispose method.
- The DbContext property will hear all repository changes.
- It Doesn’t have methods (add, remove and update), because these actions are performed through the Local DbSet property. Local is ObservableCollection<TEntity> (INotifyCollectionChanged) and it usually will be linked directly to (ListBox, ListView, DataGrid, etc).
- El repositorio conectado tiene un método SaveChanges, para mandar todos los cambios a la base de datos.
Debemos considerar el uso del repositorio conectado, ya que este tiene un fuerte impacto en el número de conexiones con la base de datos. Cuando utilizamos el repositorio conectado consumimos una conexión de base de datos por pantalla y por usuario, eso quiere decir que si tenemos 5 usuarios conectados con 4 pantallas cada uno tendríamos 20 conexiones activas constantemente.
La colección ObservableCollection<T>
es la clave del repositorio ya que este es el intermediario entre las
interacciones del usuario y la máquina y su conexión con el DbSet/DbContext. ObservableCollection<T>
recibe los datos de la base de datos mediante consultas de selección y escucha
los cambios de tamaño mediante los métodos Add y Remove y su evento CollectionChanged, mientras sus
actualizaciones se contralan mediante el INotifiedPropertyChanged
de cada uno de los objetos que forman el la colección del ObservableCollection.
El método Set<TEntity>
DbContext
Set<TEntity> es completamente igual de importante que para el anterior Disconnected Repository, aunque este caso, el repositorio conectado salva su referencia como campo interno de la clase, ya que éste al igual que el DbSet, se mantienen vivo durante todo el ciclo de vida.
Para más información sobre este método podemos ver la
entrada del repositorio
desconectado que se explica más minuciosamente
Clases de ejemplo
Estas son las clases de ejemplo:
public partial class MyDBEntities : DbContext { public MyDBEntities() : base("name=MyDBEntities") { } public virtual DbSet<City> Cities { get; set; } public virtual DbSet<FootballClub> FootballClubs { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<City>() .Property(e => e.Name) .IsUnicode(false); modelBuilder.Entity<FootballClub>() .Property(e => e.Name) .IsUnicode(false); modelBuilder.Entity<FootballClub>() .Property(e => e.Members) .HasPrecision(18, 0); } } public partial class City { public int Id { get; set; } [Required] [StringLength(50)] public string Name { get; set; } [Column(TypeName = "numeric")] public decimal? People { get; set; } [Column(TypeName = "numeric")] public decimal? Surface { get; set; } public ICollection<FootballClub> FootballClubs { get; set; } } public partial class FootballClub { public int Id { get; set; } public int CityId { get; set; } [Required] [StringLength(50)] public string Name { get; set; } [Column(TypeName = "numeric")] public decimal Members { get; set; } [Required] [StringLength(50)] public string Stadium { get; set; } [Column(TypeName = "date")] public DateTime? FundationDate { get; set; } public string Logo { get; set; } }
Construyendo un Entity Framework
Generic Repositories Desconectado
El primer paso es crear la clase ConGenericRepository:
public class ConGenericRepository<TEntity> : IDisposable where TEntity : class { protected internal readonly DbContext _dbContext; protected internal readonly DbSet<TEntity> _dbSet; public ConGenericRepository(DbContext dbContext) { if (dbContext == null) throw new ArgumentNullException(nameof(dbContext), $"The parameter dbContext can not be null"); _dbContext = dbContext; _dbSet = _dbContext.Set<TEntity>(); } public void Dispose() { if (_dbContext != null) _dbContext.Dispose(); } }
Para comenzar, inyectaremos el objeto DbContext y lo
guardaremos en un campo del mismo tipo.
La clase deberá implementar la interfaz IDisposable para
liberar los recursos no administrados.
La clase tendrá una restricción a nivel genérica para tipos
por referencia.
Varios de sus métodos son muy similares al repository
desconectado en su descripción, pero con mucha diferencia en su implementación.
Vamos a ver sus métodos.
public ObservableCollection<TEntity> All() { _dbSet.Load(); var result = _dbSet.Local; return result; } public Task<ObservableCollection<TEntity>> AllAsync() { return Task.Run(() => { return All(); }); }
En uso:
[TestMethod] public void All_OK() { ObservableCollection<FootballClub> result = instance.All(); Assert.IsNotNull(result); Assert.IsTrue(result.Count > 0); }
El método All/Async carga en la propiedad Local
(ObservableCollection) la totalidad de los datos de la tabla y devuelve la
colección. La propiedad Local, continuará escuchando los cambios.
El método Find/FindAsync, es muy similar a All/AllAsync, pero
Find/FindAsync con la diferencia de que
Finde busca por PK. La PK puede ser simple o compuesta. Este método
devuelve un único registro.
public TEntity Find(params object[] pks) { if (pks == null) throw new ArgumentNullException(nameof(pks), $"The parameter pks can not be null"); var result = _dbSet.Find(pks); return result; } public Task<TEntity> FindAsync(object[] pks) { return Task.Run(() => { return Find(pks); }); }
El parámetro pks tiene el mismo comportamiento que para el
repositorio desconectado, ver esta sección del artículo para más información.
En uso:
[TestMethod] public void Find_OK() { object[] pks = new object[] { 1 }; FootballClub result = instance.Find(pks); Assert.AreEqual(result.Id, 1); }
Como Find/FindAsync ,
el método GetData/GetDataAsync , es muy
similar al método All/AllAsync , con la
diferencia de que GetData tienen un
parámetro de tipo Expression<Func<TEntity,bool>>
para filtrar la consulta. El método Find/FindAsync
solo devuelve un registro mientras que GetData/GetDataAsync
, devuelve un conjunto de registros, aunque su filtro devuelva un único dato.
public ObservableCollection<TEntity> GetData(Expression<Func<TEntity, bool>> filter) { if (filter == null) throw new ArgumentNullException(nameof(filter), $"The parameter filter can not be null"); _dbSet.Where(filter).Load(); var filterFunc = filter.Compile(); var result = new ObservableCollection<TEntity>(_dbSet.Local.Where(filterFunc)); RelinkObservableCollection(result); return result; } public Task<ObservableCollection<TEntity>> GetDataAsync(Expression<Func<TEntity, bool>> filter) { return Task.Run(() => { return GetData(filter); }); }
Para la ejecución del método Find, nos apoyaremos en el método
privado RelinkObservableCollection:
private void RelinkObservableCollection(ObservableCollection<TEntity> result) { result.CollectionChanged += (sender, e) => { switch (e.Action) { case System.Collections.Specialized.NotifyCollectionChangedAction.Add: _dbSet.Add((TEntity)e.NewItems[0]); break; case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: _dbSet.Remove((TEntity)e.OldItems[0]); break; default: break; } }; }
Este de método de apoyo es necesario, ya que al devolver un
nuevo elemento ObservableCollection<T>, la propiedad Local se desvincula
y se dejarían de realizar los cambios, por eso antes de devolver la colección
la revinculamos.
En uso:
[TestMethod] public void GetData_OK() { Expression<Func<FootballClub, bool>> filter = a => a.Name == "Real Madrid C. F."; ObservableCollection<FootballClub> result = instance.GetData(filter); Assert.IsNotNull(result); Assert.IsTrue(result.Count == 1); }
SaveChanges / SaveChangesAsync
El método SaveChanges/SaveChangesAsync preserva los cambios
de la propiedad Local (ObservableCollection) en la base de datos.
public int SaveChanges() { var result = _dbContext.SaveChanges(); return result; } public Task<int> SaveChangesAsync() { return _dbContext.SaveChangesAsync(); }
En acción:
[TestMethod] public void SaveChanges_OK() { ObservableCollection<FootballClub> data = instance.All(); data.Add(new FootballClub { CityId = 1, Name = "New Team", Members = 0, Stadium = "New Stadium", FundationDate = DateTime.Today }); int result = instance.SaveChanges(); int expected = 1; RemovedInsertRecords(); Assert.AreEqual(expected, result); }
HasChanges / HasChangesAsync
El método HasChanges/HasChangesAsync verifica si la propiedad DbSet ha sido modificada. En las aplicaciones WPF esto es muy práctico en conjunción con los Commands para habilitar/Deshabilitar los botones de salvado de datos.
public bool HasChanges() { var result = _dbContext.ChangeTracker.Entries<TEntity>() .Any(a => a.State == EntityState.Added || a.State == EntityState.Deleted || a.State == EntityState.Modified); return result; } public Task<bool> HasChangesAsync() { return Task.Run(() => { return HasChanges(); }); }
El método verifica la propiedad ChangeTracker buscando las
entidades en estado Added, Deleted o Modified.
En acción:
[TestMethod] public void HasChanges_OK() { ObservableCollection<FootballClub> data = instance.All(); data.Add(new FootballClub { CityId = 1, Name = "New Team", Members = 0, Stadium = "New Stadium", FundationDate = DateTime.Today }); bool result = instance.HasChanges(); Assert.IsTrue(result); }
Una vez que esto está hecho extrairemos la Interface.
Resultado:
public interface IConGenericRepository<TEntity> : IDisposable where TEntity : class { ObservableCollection<TEntity> All(); Task<ObservableCollection<TEntity>> AllAsync(); ObservableCollection<TEntity> GetData(Expression<Func<TEntity, bool>> filter); Task<ObservableCollection<TEntity>> GetDataAsync(Expression<Func<TEntity, bool>> filter); TEntity Find(params object[] pks); Task<TEntity> FindAsync(object[] pks); int SaveChanges(); Task<int> SaveChangesAsync(); bool HasChanges(); Task<bool> HasChangesAsync(); }
Trasladaremos la interfaz IDisposable de la clase a nuestra
nueva interfaz.
El ejemplo es muy similar al anterior del Repositorio Desconectado.
Pensando directamente en nuestro repositorio conectado, la
clase MainViewModel tiene mucha importancia. En nuestro ejemplo contiene la
vinculación de nuestro Datagrid con la propiedad ObservableCollection y realiza
los cambios automáticamente mediante estas tres acciones:
- Insert .- Para inserter registros, haremos doble click en la última fila datagridrow.
- Update .- Para actualizar los registros, haremos un doble click en un datagridcell para entrar en modo edición y modificar la entidad.
- Delete .- Para eliminar registros, seleccionaremos el datagridrow y pulsaremos la tecla ‘supr’.
- rows, we will select the datagridrow and press the ‘supr’ key.
Ahora mostraremos todas las clases de tipo ViewModel donde se usa el proyecto Connected Generic Repository de WPF.
public class MainViewModel : ViewModelBase, IDisposable { private readonly IConGenericRepository<FootballClub> _repository; public ObservableCollection<FootballClub> Data { get; set; } private FootballClub _selectedItem; public FootballClub SelectedItem { get { return _selectedItem; } set { Set(nameof(SelectedItem), ref _selectedItem, value); } } public MainViewModel(IConGenericRepository<FootballClub> repository) { _repository = repository; Data = _repository.All(); } public void Dispose() { _repository.Dispose(); } public RelayCommand SaveCommand => new RelayCommand(SaveExecute, SaveCanExecute); private bool SaveCanExecute() { var result = _repository.HasChanges(); return result; } private void SaveExecute() { Action callback = () => { var changes = _repository.SaveChanges(); Messenger.Default.Send(new PopupMessage($"It has been realized {changes} change(s) in Database." )); CollectionViewSource.GetDefaultView(Data).Refresh(); }; Messenger.Default.Send(new PopupMessage("Do you want to make changes in DataBase ?", callback)); } }
La clase MainViewModel, recibe injectado una interfaz de
tipo IConGenericRepository<FootballClub>. En el constructor regeneraremos
nuestro campo del mismo tipo y usaremos el método All para poblar nuestro
respositorio genérico. Esta clase tiene un RelayComand con el nombre de SaveCommand.
Este command usa dos métodos, Execute y CanExecute. Para CanExecute, usaremos
el método HasChanges de nuestro repositorio genérico, que habilitará o
deshabilitará el botón salvar, según detecte si se han producido cambios en el
contexto. El método SaveExecuted, enviará un mensaje a la vista, que mostrará
un messagebox y esperará la respuesta del usuario para en caso afirmativo,
lanzar el método SaveChanges de nuestro repositorio genérico y mandar los
cambios a base de datos.
Extending ConGenericRepository
The
connected world, can be confused. In the previous example, we could make
serveral changes in the datagrid and as we were going to save the changes, we
knew nothing of which rows are inserted or which rows are updated or deleted.
For this reason, we will create a new datagrid column with this information.
This column will be the row state.
Dentro de la clase del modelo, crearemos una nueva propiedad
NotMapped (esta propiedad no interferirá en la base de datos).
private string _state; [NotMapped] public string State { get { return _state; } set { if (_state != value) { _state = value; OnPropertyChanged(); } } }
Esta propiedad contendrá todos los cambios generales de estado.
Algo similar a lo que contenían las clases creadas automáticamente por Entity
Framework en sus primeras versiones.
Hecho este paso, crearemos nuestro repositorio conectado
especifico, FootballClubConRepository.
public class FootballClubConRepository : ConGenericRepository<FootballClub>, IFootballClubConRepository { public FootballClubConRepository(DbContext dbContext) : base(dbContext) { } public string GetState(FootballClub entity) { var stateEntity = _dbContext.Entry(entity).State; return stateEntity.ToString(); } }
Este nuevo repositorio deberá heredar de la clase ConGenericRepository<TEntity>
e implementar su constructor base.
Añadiremos el método GetState para consultar el estado
general interno que nos proporciona Entity Framework.
Ahora vamos a actualizar nuestra clase MainViewModel:
public class MainViewModel : ViewModelBase, IDisposable { private readonly IFootballClubConRepository _repository; //private readonly IConGenericRepository<FootballClub> _repository; public ObservableCollection<FootballClub> Data { get; set; } //public MainViewModel(IConGenericRepository<FootballClub> repository) public MainViewModel(IFootballClubConRepository repository) { _repository = repository; Data = _repository.All(); ListenerChangeState(Data, _repository); } private void ListenerChangeState(ObservableCollection<FootballClub> data, IFootballClubConRepository repository) { data.ToList().ForEach(a => ChangeStateRegister(a, repository)); data.CollectionChanged += (sender, e) => { if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) { var entity = e.NewItems[0] as FootballClub; entity.State = "Added"; } }; } private void ChangeStateRegister(FootballClub entity, IFootballClubConRepository repository) { entity.PropertyChanged += (sender, e) => { if (e.PropertyName != "State") { entity.State = repository.GetState(entity); } }; } public void Dispose() { _repository.Dispose(); } public RelayCommand SaveCommand => new RelayCommand(SaveExecute, SaveCanExecute); private bool SaveCanExecute() { var result = _repository.HasChanges(); return result; } private void SaveExecute() { Action callback = () => { var changes = _repository.SaveChanges(); Messenger.Default.Send(new PopupMessage($"It has been realized {changes} change(s) in Database." )); CollectionViewSource.GetDefaultView(Data).Refresh(); ResetDataStates(Data); }; Messenger.Default.Send(new PopupMessage("Has you make the changes in DataBase ?", callback)); } private void ResetDataStates(ObservableCollection<FootballClub> data) { data.ToList().ForEach(a => a.State = null); } }
Hemos añadido dos métodos privados para registrar los
cambios, ListenerChangedState, que registra los cambios de inserciones y ChangeStateRegister
que registra los cambios de las modificaciones.
Por último mostraremos la clase converter que transforma
nuestro estado en imágenes:
public class StateConverter : IMultiValueConverter { public ImageBrush _imgInsert; public ImageBrush _imgUpdate; public StateConverter() { _imgInsert = Application.Current.FindResource("Inserted") as ImageBrush; _imgUpdate = Application.Current.FindResource("Edited") as ImageBrush; } public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values[0] == null) return null; var valueStr = values[0].ToString(); switch (valueStr) { case "Added" : return _imgInsert; case "Modified": return _imgUpdate; } return null; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
La clase transforma la descripción del estado en una imagen:
In action:
public class StateConverter : IMultiValueConverter { public ImageBrush _imgInsert; public ImageBrush _imgUpdate; public StateConverter() { _imgInsert = Application.Current.FindResource("Inserted") as ImageBrush; _imgUpdate = Application.Current.FindResource("Edited") as ImageBrush; } public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values[0] == null) return null; var valueStr = values[0].ToString(); switch (valueStr) { case "Added" : return _imgInsert; case "Modified": return _imgUpdate; } return null; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
La clase transforma la descripción del estado en una imagen:
In action:
Proyecto de Pruebas
El proyecto de prueba es el mismo que para la versión
anterior Generic
Repository Disconnected. Hemos añadido un nuevo proyecto WPF llamado BuildingEFGRepository.WPF_Con
para nuestro nuevo repositorio conectado.
Se puede descargar de github en este link.
No hay comentarios :
Publicar un comentario