Introduction
A few weeks ago, we looked at a first article about of Disconnected Repository, in this article, let’s complete with other piece off the puzzle, the connected Generic Repository. In this new Repository type, a very important new actor appears, is none other than ObservableCollection<T>
, this will be an inseparable friend for Connected Generic Repository.
Index
Entity Framework Generic Repositories Connected
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
. This generic repository type works with direct DataGrid
modifications.
Its main characteristics are:
- Should receive the
DbContext
from dependency injection - Should implement
IDisposable
interface for releasing unmanaged resources - Should have 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.). - The repository connected has a
SaveChanged
method for send all changes to database.
It has a SaveChanged
method.
We must consider Connected Repository use, because it has much impact on connection database consumption. This process consumes a connection for each user and screen loaded.
The ObservableCollection<T>
is the key for the Repository
, it is the intermediary between user/machine interactions and DbSet
/DbContext
. The ObservableCollection<T>
receives the data from database through queries and listen the insert
/delete
changes through your event CollectionChanged
and the modified by the event INotifiedPropertyChanged
of the model.
Set<TEntity> DbContext method
Set<TEntity>
is the same as in the Disconnected Repository is very important, but in this case, our Connected Repository saves its reference in a protected
field, because it has to be available in all repository live.
For more information, you read the DbSet<T>
section in Disconnected Repository.
Example Classes
This is the example classes:
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; }
}
Building Entity Framework Generic Repositories Disconnected
In the first step, we will create the generic ConGenericRepository
class:
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();
}
}
To start, we will inject the DbContext
object, will consult the DbSet
and will save in a dbset
field.
The class should implement IDisposible
interface for releasing the unmanage resources.
The class has a generic constraint from reference types.
Some methods are very similar that Disconnected in description, but are different in implementation.
Let’s go to build all methods.
All / AllAsync
The All
/AllAsync
methods return the All
table data.
public ObservableCollection<TEntity> All()
{
_dbSet.Load();
var result = _dbSet.Local;
return result;
}
public Task<ObservableCollection<TEntity>> AllAsync()
{
return Task.Run(() =>
{
return All();
});
}
In use:
[TestMethod]
public void All_OK()
{
ObservableCollection<FootballClub> result = instance.All();
Assert.IsNotNull(result);
Assert.IsTrue(result.Count > 0);
}
The method All
/Async
loads the complete table in the Local (ObservableCollection
) property and returns the collection. The local property continuously listens to the changes.
Find / FindAsync
The Find
/FindAsync
methods, is very similar to All
/AllAsync
methods, but Find
/FindAsync
searches a simple row for PK
. The PK
can be simple or complex. Return one row always.
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 _dbSet.FindAsync(pks);
}
The param pks
behavior is identical to Disconnected, view this section of Disconnected article for more information.
In use:
[TestMethod]
public void Find_OK()
{
object[] pks = new object[] { 1 };
FootballClub result = instance.Find(pks);
Assert.AreEqual(result.Id, 1);
}
GetData / GetDataAsync
Like Find
/FindAsync
, the methods GetData
/GetDataAsync
are very similar than All
/AllAsync
unlike, GetData
has an Expression<Func<TEntity,bool>>
parameter for filter the query and the Find
/FindAsync
return only one item and GetData
/GetDataAsync
returns a collection ever although the collection has one item.
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);
});
}
Note that we have used a private
method 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;
}
};
}
This method is necessary because we should return a part of DbSet
property Local information only. For it, we create a new ObservableCollection
with the filter data and in this moment, the Local property unlink it. The RelinkObservableCollection
relinked the ObservableCollection
changes with the DbSet
.
In use:
[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
The method SaveChanges
/SaveChangesAsync
preserves the ObservableCollection Local
property in database.
public int SaveChanges()
{
var result = _dbContext.SaveChanges();
return result;
}
public Task<int> SaveChangesAsync()
{
return _dbContext.SaveChangesAsync();
}
In action:
[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
The method HasChanges
/HasChangesAsync
verifies if the DbSet
property has been modified. In WPF applications, this is very practical in conjunction of Commands for enabled or disabled save buttons.
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();
});
}
The method verified the ChangeTracker
property search rows in state: Added
, Deleted
or Modified
.
In action:
[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);
}
Once this has been done, we will extract the Interface.
Result:
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();
}
We have translated the IDisposable
implements to this interface.
WPF Example
The example making is the same to for Disconnected Repository, so that you can see it in the Disconnected article.
Thinking in our Connected Generic Repository, MainViewModel
is the most important class. In our example, we interact directly with the datagrid
for three actions:
Insert
- For insert new row, we will fill the last empty datagridrow
. Update
- For update rows, we will click the datagridcell
for entry in edit mode, and we will modify data. Delete
- For delete rows, we will select the datagridrow
and press the ‘supr
’ key.
In Action:
In the following, we will show the classes (ViewModels
) where we use the Connected Generic Repository in the WPF project.
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));
}
}
The class MainViewModel
receives injected a IConGenericRepository<FootballClub>
interface. In its constructor, feed your namesake inject field and fill the ObservableCollection
uses All generic repository method. This class has a RelayCommand
with name SaveCommand
, this command uses two methods, Execute
and CanExecute
. For CanExecute
method, we will use the HasChanges
repository method, this will provide enabled or disabled the Save button and we will us to assure save without changes. The SaveExecuted
method sends message to view for show messagebox, and it waits for the messagebox
answer to save data in database for SaveChanges
repository method.
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.
In the Enity Framework model class, we will create a new NotMapped
property:
private string _state;
[NotMapped]
public string State
{
get { return _state; }
set
{
if (_state != value)
{
_state = value;
OnPropertyChanged();
}
}
}
This property will contain the all property general state. In Entity Framework initial version, all generates entities class had this property.
We will add the new specific generic repository connected, FutballClubConRepository
:
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();
}
}
FutballClubConRepository
should be inherits ConGenericRepository<TEntity>
and implements a constructor base. Add the GetState
method for advice the Entity Framework internal state.
We will update MainViewModel
:
public class MainViewModel : ViewModelBase, IDisposable
{
private readonly IFootballClubConRepository _repository;
public ObservableCollection<FootballClub> Data { get; set; }
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);
}
}
We have added two private
methods for register changes ListenerChangedState
, that register the insert
changes and ChangeStateRegister
that register the modified changes.
Ultimately, we will review the class converter:
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();
}
}
This class transforms the state description for the image description.
In action:
Test Project
The test project still having the same structure that the previous article Generic Repository Disconnected. We add a new WPF project BuildingEFGRepository.WPF_Con
with the new example.
You will the change connectionstring
of project BuildingEFGRepository.WPF_Con
too.
History
- 18th December, 2017: Initial version