65.9K
CodeProject 正在变化。 阅读更多。
Home

构建 Entity Framework 通用存储库 2 连接

2017 年 12 月 18 日

CPOL

6分钟阅读

viewsIcon

15879

downloadIcon

267

继续使用 Entity Framework 通用存储库,在本例中是为有状态应用程序连接。

引言

几周前,我们回顾了第一篇关于 断开连接存储库 的文章,在这篇文章中,我们将完成另一块拼图,即连接的通用存储库。在这种新的存储库类型中,出现了一个非常重要的全新角色,它就是 ObservableCollection<T>,这将是连接通用存储库不可分割的朋友。

目录

Entity Framework 通用存储库 已连接

Entity Framework 通用存储库已连接,用于 WPF、Silverlight、Windows Forms、控制台应用程序等状态处理。

此存储库处理组更改,并已修复到 ItemsControls。此通用存储库类型可直接处理 DataGrid 的修改。

其主要特点是

  • 应通过依赖注入接收 DbContext
  • 应实现 IDisposable 接口以释放非托管资源
  • 应有一个 DbContext 受保护的属性。此属性将在通用存储库的生命周期内保持活动状态,仅在 Dispose 方法中终止。
  • DbContext 属性将监听存储库的所有更改。
  • 它没有方法(添加、删除和更新),因为这些操作是通过 Local DbSet 属性执行的。Local 是 ObservableCollection<TEntity> (INotifyCollectionChanged),通常会直接链接到(ListBox、ListView、DataGrid 等)
  • 已连接的存储库有一个 SaveChanged 方法,用于将所有更改发送到数据库。

它有一个 SaveChanged 方法。

我们必须考虑连接存储库的使用,因为它对数据库连接消耗有很大影响。此过程为每个用户和加载的屏幕消耗一个连接。

ObservableCollection<T>Repository 的关键,它是用户/机器交互与 DbSet/DbContext 之间的中间件。ObservableCollection<T> 通过查询接收数据库中的数据,并通过其事件 CollectionChanged 监听 insert/delete 更改,并通过模型的事件 INotifiedPropertyChanged 监听修改。

Set<TEntity> DbContext 方法

Set<TEntity>断开连接的存储库 中的一样非常重要,但在这种情况下,我们的已连接存储库将其引用保存在一个 protected 字段中,因为它必须在存储库的整个生命周期内可用。

有关更多信息,请阅读 断开连接的存储库DbSet<T> 部分。

示例类

这是示例类

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; }
}

构建 Entity Framework 通用存储库 已断开连接

第一步,我们将创建通用的 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();
    }
}

首先,我们将注入 DbContext 对象,查询 DbSet 并将其保存在 dbset 字段中。

该类应实现 IDisposible 接口以释放非托管资源。

该类具有引用类型的泛型约束。

有些方法的描述与断开连接的非常相似,但在实现上有所不同。

让我们开始构建所有方法。

All / AllAsync

All/AllAsync 方法返回 All 表数据。

public ObservableCollection<TEntity> All()
{
    _dbSet.Load();

    var result = _dbSet.Local;

    return result;
}

public Task<ObservableCollection<TEntity>> AllAsync()
{
    return Task.Run(() =>
    {
        return All();
    });
}

使用中

[TestMethod]
public void All_OK()
{
    ObservableCollection<FootballClub> result = instance.All();

    Assert.IsNotNull(result);
    Assert.IsTrue(result.Count > 0);
}

方法 All/Async 将整个表加载到 Local (ObservableCollection) 属性中并返回该集合。Local 属性会持续监听更改。

Find / FindAsync

Find/FindAsync 方法与 All/AllAsync 方法非常相似,但 Find/FindAsync 通过 PK 搜索单个行。PK 可以是简单的或复杂的。始终返回一行。

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);
}

param pks 的行为与 断开连接 相同,有关更多信息,请参阅 断开连接文章 的此部分。

使用中

[TestMethod]
public void Find_OK()
{
    object[] pks = new object[] { 1 };

    FootballClub result = instance.Find(pks);

    Assert.AreEqual(result.Id, 1);
}

GetData / GetDataAsync

Find/FindAsync 一样,GetData/GetDataAsync 方法与 All/AllAsync 非常相似,不同之处在于,GetData 有一个 Expression<Func<TEntity,bool>> 参数用于过滤查询,而 Find/FindAsync 只返回一个项目,GetData/GetDataAsync 则始终返回一个集合,即使该集合只有一个项目。

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);
    });
}

请注意,我们使用了一个 private 方法 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;
        }
    };
}

此方法是必需的,因为我们应该只返回 DbSet 属性 Local 信息的一部分。为此,我们创建一个新的 ObservableCollection 包含过滤后的数据,此时 Local 属性会解除与其的链接。RelinkObservableCollection 会重新将 ObservableCollection 的更改与 DbSet 链接起来。

使用中

[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

SaveChanges/SaveChangesAsync 方法会将 ObservableCollection Local 属性保存在数据库中。

public int SaveChanges()
{
    var result = _dbContext.SaveChanges();

    return result;
}

public Task<int> SaveChangesAsync()
{
    return _dbContext.SaveChangesAsync();
}

实际运行

[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

HasChanges/HasChangesAsync 方法验证 DbSet 属性是否已被修改。在 WPF 应用程序中,这与 Commands 结合使用非常实用,可以启用或禁用保存按钮。

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();
    });
}

该方法验证 ChangeTracker 属性,查找状态为:AddedDeletedModified 的行。

实际运行

[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);
}

提取接口

完成此操作后,我们将提取接口。

结果

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();
}

我们将 IDisposable 实现翻译到了这个接口。

WPF 示例

示例的创建与断开连接存储库相同,您可以在 断开连接文章 中看到它。

考虑到我们的连接通用存储库,MainViewModel 是最重要的类。在我们的示例中,我们直接与 datagrid 进行交互,进行三个操作:

  1. Insert - 要插入新行,我们将填充最后一个空白的 datagridrow
  2. Update - 要更新行,我们将点击 datagridcell 进入编辑模式,然后修改数据。
  3. Delete - 要删除行,我们将选择 datagridrow 并按“supr”键。

实际应用

接下来,我们将展示在 WPF 项目中使用连接通用存储库的类(ViewModels)。

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));
    } 
}

MainViewModel 类通过注入接收 IConGenericRepository<FootballClub> 接口。在其构造函数中,填充同名的注入字段,并使用 All 通用存储库方法填充 ObservableCollection。此类有一个名为 SaveCommandRelayCommand,该命令使用两个方法:ExecuteCanExecute。对于 CanExecute 方法,我们将使用 HasChanges 存储库方法,这将使“保存”按钮启用或禁用,并确保在没有更改的情况下保存。SaveExecuted 方法向视图发送消息以显示 messagebox,并等待 messagebox 的答复,以便通过 SaveChanges 存储库方法将数据保存到数据库。

扩展 ConGenericRepository

连接的世界可能会令人困惑。在上一个示例中,我们可以在 datagrid 中进行多次更改,当我们保存更改时,我们不知道哪些行被插入,哪些行被更新或删除。因此,我们将创建一个新的 datagrid 列来提供此信息。该列将是行的状态。

在 Entity Framework 模型类中,我们将创建一个新的 NotMapped 属性

private string _state;
[NotMapped]
public string State
{
    get { return _state; }
    set
    {
        if (_state != value)
        {
            _state = value;
 
            OnPropertyChanged();
        } 
    }
}

此属性将包含所有属性的通用状态。在 Entity Framework 的初始版本中,所有生成的实体类都包含此属性。

我们将添加新的特定连接通用存储库,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();
    } 
}

FutballClubConRepositoryinherits ConGenericRepository<TEntity> 并实现一个基构造函数。添加 GetState 方法以告知 Entity Framework 的内部状态。

我们将更新 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);
    }
}

我们添加了两个 private 方法来注册更改:ListenerChangedState,用于注册 insert 更改;ChangeStateRegister,用于注册修改后的更改。

最后,我们将回顾转换类

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();
    }
}

此类将状态描述转换为图像描述。

实际运行

测试项目

测试项目的结构与上一篇文章 Generic Repository Disconnected 相同。我们添加了一个新的 WPF 项目 BuildingEFGRepository.WPF_Con 来实现新示例。

您还将看到项目 BuildingEFGRepository.WPF_Conconnectionstring 发生了变化。

历史

  • 2017 年 12 月 18 日:初始版本
© . All rights reserved.