lunes, 1 de enero de 2018

Construyendo un Entity Framewor Generic Repository Conectado





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.



El método All/AllAsync  devuelve la tabla completa.


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.
The example making is the same to for Disconnected Repository.

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