通过GridView绑定flattern EF实体并允许与存储库同步

时间:2014-07-04 21:21:16

标签: c# linq entity-framework datagridview flatten

我在复杂的网格数据绑定情况下第一次使用Entity Framework,我需要一些策略来完成这项任务。 我已经通过“EF POCO反向代码优先”自动生成了这些POCO类(对于映射是相同的),并且每个类都有一个存储库,我用它来查询或执行简单的CRUD操作:

    public class Order
    {
        public int IdOrder { get; set; }
        public int IdCustomer { get; set; }
        public int IdOrderType {get; set;}
        public string ChOrder { get; set; }
        public decimal Total { get; set; }

        public Customer Customer { get; set; }
        public OrderType OrderType {get; set;}
    }

    public class OrderType 
    {
        public int IdOrderType {get; set;}
        public string ChOrderType {get; set;}
        public string Description { get; set; }
    }

    public class Customer
    {
        public int IdCustomer { get; set; }
        public string ChCustomer { get; set;}
        public string Name { get; set; }
        public string City { get; set; }
        public CustomerType CustomerType { get; set;}
    }

    public class CustomerType
    {
        public int IdCustomerType { get; set; }
        public string ChCustomerType { get; set; }
    }

现在我想查询OrderRepository并在DataGridView中显示所有结果(在实际情况下,它是DevExpress XtraGrid)。此DataGridView必须显示“直接”订单信息(ChOrder,Total),还必须显示“间接”信息,例如可通过使用导航属性(例如Customer.Name,Customer.City,Customer.CustomerType.ChCustomerType)访问的信息。 我想要的是这样的事情:

enter image description here

在“Readonly DataGridView”场景中,此任务非常简单。您只需要创建一个linq查询来平坦存储库的结果集并将其绑定到网格,如下所示:

this.OrderRepository.AsQueryable().Select(x => new { .ChOrder = x.ChOrder, 
                             .Total = x.Total, 
                             .ChOrderType = x.OrderType.ChOrderType, 
                             .ChCustomer = x.ChCustomer, 
                             .Name = x.Customer.Name, 
                             .City = x.Customer.City, 
                             .CustomerType = x.Customer.CustomerType}).ToList();

但网格必须是可编辑的。特别是我想添加新订单,修改ChOrderType(及其描述),修改ChCustomer(以及他的姓名,城市和类型)并删除订单。我无法绑定LINQ结果,因为:

  1. LINQ匿名类型结果是不可变的(因此网格将是只读的)
  2. LINQ生成一个列表,其中项目可以更改(用户可以删除项目,添加新项目,修改等等),并且由于它是一个列表,因此无法跟踪已删除,添加和修改的项目(即使我使用更合适的BindingList或ObservableCollection)。所以我不知道哪些实体会删除,更新或插入。
  3. LINQ结果项不实现INotifyPropertyChanged,因此,如果我更改例如“ChCustomer”,其相关数据(Name,City ec ..)将不会在屏幕上更新。
  4. 由于这些问题,我试图以这种方式直接绑定POCO OrderClass:

    this.OrderRepository.AsQueryable().Select()
    

    将“间接”字段设置为“DataPropertyName”,如下所示:

      

    列“名称” - > Customer.Name

         

    列“城市” - > Customer.City

    但同样,它不适合因为:

    1. 我的POCO类没有实现INotifyPropertyChanged,因此,如果我更改例如“ChCustomer”,它的相关数据(Name,City ec ..)将不会在屏幕上更新。

    2. 当我编辑ChCustomer时,显然数据绑定不知道如何根据用户插入的ChCustomer从其存储库中获取Customer实例,并将其分配给当前的Order“Customer”属性。

    3. 有时,可编辑字段(如ChCustomers)是复合键的一部分,实际上EF中不允许编辑实体的复合键。这引入了更多问题。

    4. 因此,在故事的最后,事实证明,当EF涉及数据绑定时,管理是一项艰巨的任务(在我们的软件中有数千个这样的情况)。

      我想知道是否有一个很好的策略来完成这项任务。

      我想通过使用ViewModel(也实现INotifyPropertyChanged)对象绑定到gridView来解决这个问题:

      public class OrderViewModel : INotifyPropertyChanged{
      
              public event PropertyChangedEventHandler PropertyChanged;
      
              //Order info
              public string ChOrder { get; set; }
              public decimal Total {get; set;}
              public string ChOrderType {get; set;}
              public string Description { get; set; }
      
              //Customer info
              public string ChCustomer { get; set; }
              public string Name { get; set;}
              public string City { get; set;}
              public string ChCustomerType {get; set;}
          }
      

      它工作得很好,但我不知道如何与存储库同步数据。 特别是如何跟踪已删除的项目并使用存储库删除它们? 如何追踪添加的项目?编辑会改变吗? 还有......如何设置所有来自导航属性的属性(如名称,城市等)? 当我调用UnitOfWork的SaveChanges()时,我想将我的所有chages正确地保存到DB。

      对不起,如果我说话,但我尽力做到最清楚。 提前谢谢。

2 个答案:

答案 0 :(得分:1)

您可以使用ViewModel方法将您的表示逻辑与View粘合在一起。您不会使用它来表示绑定到数据网格的示例中的单个记录,并且由于支持双向绑定,它通常用于WPF应用程序。

使用扁平方法表示多个对象可能不是最好的主意,因为客户可以有多个订单,因此当您按照问题中的建议修改客户的详细信息时,您需要强制PropertyChanged到其他记录与同一客户。您可能希望实现的是应用程序中的主 - 详细网格视图,您可以在其中展开Customer行以查看其订单。

如果要继续使用展平结构的方法,则应创建一个包装类来包含从存储库加载的实体。然后,您将在包装类上公开属性,引用基础实体的值属性。例如:

public class BindableRecord : INotifyPropertyChanged
{
    Order _OrderObject;
    internal Order OrderObject
    {
        get { return _OrderObject; }
        set
        {
            _OrderObject = value;
            _OrderObject.PropertyChanged += OrderObject_PropertyChanged;
        }
    }

    void OrderObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, e);
    }

    internal Customer CustomerObject { get; set; }

    internal CustomerType CustomerTypeObject { get; set; }

    public string CustomerName
    {
        get { return this.CustomerObject.Name; }
        set { this.CustomerObject.Name = value; }
    }

    public string CustomerType
    {
        get { return this.CustomerTypeObject.ChCustomerType; }
        set { this.CustomerTypeObject.ChCustomerType = value; }
    }

    public int OrderID
    {
        get { return this.OrderObject.IdOrder; }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

实体应该从INotifyPropertyChanged派生,这样您就可以向BindableRecord发送通知,然后可以将通知转发到DataGridView。此处提供了从INotifyPropertyChanged派生实体的示例: How to get property change notifications with EF 4.x DbContext generator

然后你会有一个Repository,它返回BindableRecord的

public class OrderRepository
{
    public IList<BindableRecord> Get()
    {
        using (EntityContext ctx = new EntityContext())
        {
            return (from c in ctx.Customers
                    join o in ctx.Orders on c.Order equals o
                    join ct in ctx.CustomerTypes on c.CustomerType equals ct
                    select new BindableRecord() { CustomerObject = c, CustomerTypeObject = ct, OrderObject = o }).ToList();
        }
    }

    public void Save(IEnumerable<BindableRecord> addOrUpdateEntities, IEnumerable<BindableRecord> deletedEntities)
    {
        using (EntityContext ctx = new EntityContext())
        {
            foreach (var entity in addOrUpdateEntities)
            {
                ctx.Entry(entity.CustomerTypeObject).State = entity.CustomerTypeObject.IdCustomerType == default(int) ? EntityState.Added : EntityState.Modified;
                ctx.Entry(entity.CustomerObject).State = entity.CustomerObject.IdCustomer == default(int) ? EntityState.Added : EntityState.Modified;
                ctx.Entry(entity.OrderObject).State = entity.OrderObject.IdOrder == default(int) ? EntityState.Added : EntityState.Modified;
            }
            foreach (var entity in deletedEntities)
            {
                ctx.Entry(entity.CustomerTypeObject).State = EntityState.Deleted;
                ctx.Entry(entity.CustomerObject).State = EntityState.Deleted;
                ctx.Entry(entity.OrderObject).State = EntityState.Deleted;
            }
            ctx.SaveChanges();
        }
    }
}

Save方法附加实体,然后根据它们是否已被修改,添加或删除来更新其状态。您将必须维护两个列表,一个用于已修改和已添加的实体(这将绑定到您的数据网格),另一个用于已删除的实体(这应该在删除数据网格中的记录时添加)。但是,这将更新数据库中该记录的每个字段;如果你只想更新某些字段,你可以使你的数据库上下文持久。

如果您不想维护单独的列表,可以通过从以下界面派生POCO来控制实体的状态。您可以在UI中控制此属性的状态,然后在存储库Save方法中读取此属性,而不是检查主键的默认值并维护单独的列表。

interface IEntity
{
    EntityState State { get; set;}
}

然后,您将在存储库上调用Get方法,然后将记录列表绑定到数据网格。只要您希望将更改保留回数据库,就必须在存储库中显式调用Save方法。

我希望这会有所帮助。

答案 1 :(得分:0)

为了解决这个问题,我使用了@ User978139提出的ViewModel解决方案,结构稍微复杂一些。特别是我创建了OrderViewModel,它包装了CustomerId和EmployeeId属性为Readable和Writeable的订单模型。请注意,在其setter中,我添加了一些代码来强制Customer和Employee属性进行更改,并且它们各自的PropertyChanged()事件会引发。 这是代码:

 public class OrderViewModel : IEntityViewModel, IDataErrorInfo
        {
            public event PropertyChangedEventHandler PropertyChanged;

            internal Order _order;
            INorthwindDbContext _ctx;

            public int OrderId
            {
                get { return _order.OrderId; }
            }
            public DateTime? OrderDate
            {
                get { return _order.OrderDate; }
                set
                {
                    _order.OrderDate = value;
                    RaisePropertyChanged("OrderDate");
                }
            }
            public string ShipName
            {
                get { return _order.ShipName; }
                set
                {
                    _order.ShipName = value;
                    RaisePropertyChanged("ShipName");
                }
            }
            public string ShipAddress
            {
                get { return _order.ShipAddress; }
                set
                {
                    _order.ShipAddress = value;
                    RaisePropertyChanged("ShipAddress");
                }
            }
            public string ShipCity
            {
                get
                {
                    return _order.ShipCity;
                }
                set
                {
                    _order.ShipCity = value;
                    RaisePropertyChanged("ShipCity");
                    ;
                }
            }

            public string CustomerId
            {
                get { return _order.CustomerId; }
                set
                {
                    _order.CustomerId = value;
                    var customer = this._ctx.Customers.Find(_order.CustomerId);
                    _order.Customer = customer;
                    RaisePropertyChanged("CustomerId");
                    RaisePropertyChanged("CompanyName");
                    RaisePropertyChanged("ContactName");
                }
            }
            public string CompanyName
            {
                get { return _order.Customer != null? _order.Customer.CompanyName : string.Empty; }
            }
            public string ContactName
            {
                get { return _order.Customer != null ? _order.Customer.ContactName : string.Empty; }
            }

            public int? EmployeeId
            {
                get { return _order.EmployeeId; }
                set
                {
                    _order.EmployeeId = value;
                    var employee = this._ctx.Employees.Find(_order.EmployeeId);
                    _order.Employee = employee;
                    RaisePropertyChanged("EmployeeId");
                    RaisePropertyChanged("LastName");
                    RaisePropertyChanged("FirstName");
                    RaisePropertyChanged("Title");
                }
            }
            public string LastName
            {
                get { return _order.Employee!=null? _order.Employee.LastName : string.Empty; }
            }
            public string FirstName
            {
                get { return _order.Employee!=null? _order.Employee.FirstName : string.Empty;  }
            }
            public string Title
            {
                get { return _order.Employee!=null? _order.Employee.Title : string.Empty;  }
            }

            public OrderViewModel(INorthwindDbContext ctx)
            {
                this.Init(new Order(), ctx);
            }

            public OrderViewModel(Order order, INorthwindDbContext ctx)
            {
                this.Init(order, ctx);
            }

            private void Init(Order order, INorthwindDbContext ctx)
            {
                this._order = order;
                this._ctx = ctx;
            }

            private void RaisePropertyChanged(string propname)
            {
                if (PropertyChanged != null)
                    PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propname));
            }

            public void NotifyChanges()
            {
                foreach (var prop in typeof(OrderViewModel).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.CanRead))
                    RaisePropertyChanged(prop.Name);
            }



            public string Error
            {
                get { return null; }
            }

            public string this[string columnName]
            {
                get {
                    switch (columnName)
                    {
                        case "CustomerId":
                            if (_ctx.Customers.Find(CustomerId)==null)
                                return "Invalid customer";
                            break;

                        case "EmployeeId":
                            if (_ctx.Employees.Find(EmployeeId)==null)
                                return "Invalid employee";
                            break;


                    }
                    return null;
                }
            }

        } 

然后我把&#34; Local&#34;我的OrderRepository暴露的属性(返回内存中加载和添加的实体)与我用于网格绑定的自定义BindingList实现(BindingViewModelList),它与内部repository.Local属性同步(这是一个ObservableCollection&lt; ;&gt;)反之亦然。因此,如果您在ObservableColleciton中添加内容,您也会看到BindingList中的更改,否则,如果您向BindingList添加内容(例如绑定到绑定的网格),则更改将反映到ObservableCollection太。通过这种方式,我不必追踪ViewModels对象的状态,并手动将更改反映到存储库,这是一项非常痛苦的工作。 使用此解决方案,它就像存储库的Local属性直接绑定到我用于绑定的绑定列表。从网格添加和删除记录包括从BindingList和Local属性中删除实体。因此,如果我调用SaveChanges(),所有更改都会保留到DB而不显式同步。

我希望有更好的清洁解决方案将模型集合映射到ViewModels集合,但建议的解决方案效果很好。 这是我的BindingViewModelList:

的代码
public class BindingViewModelList<TViewModel, TModel> : BindingList<TViewModel>
{
    private readonly ObservableCollection<TModel> _source;
    private readonly Func<TModel> _modelFactory;
    private readonly Func<TModel, TViewModel> _viewModelFactory;
    private readonly Func<TViewModel, TModel> _getWrappedModel;

    private bool isSync = false;

    public BindingViewModelList(ObservableCollection<TModel> source, Func<TModel, TViewModel> viewModelFactory, Func<TModel> modelFactory, Func<TViewModel, TModel> getWrappedModel)
        : base(source.Select(model => viewModelFactory(model)).ToList())
    {
        Contract.Requires(source != null);
        Contract.Requires(viewModelFactory != null);

        this._source = source;
        this._modelFactory = modelFactory;
        this._viewModelFactory = viewModelFactory;
        this._getWrappedModel = getWrappedModel;

        this._source.CollectionChanged += OnSourceCollectionChanged;
        this.AddingNew += BindingViewModelList_AddingNew;
    }


    protected virtual TModel CreateModel()
    {
        return _modelFactory.Invoke(); 
    }

    protected virtual TViewModel CreateViewModel(TModel model)
    {
        return _viewModelFactory(model);
    }


    private void BeginSync()
    {
        this.isSync = true;
    }

    private void EndSync()
    {
        this.isSync = false;
    }


    void BindingViewModelList_AddingNew(object sender, AddingNewEventArgs e)
    {
        e.NewObject = CreateViewModel(CreateModel());
    }


    protected override void OnListChanged(ListChangedEventArgs e)
    {
        if (!this.isSync)
        {

            if (e.NewIndex >= 0 && e.NewIndex < this.Count)
            {
                bool ok = true;
                TViewModel item = default(TViewModel);
                try
                {
                    item = this[e.NewIndex];
                }
                catch (IndexOutOfRangeException ex)
                {
                    ok = false;
                }

                if (ok)
                {
                    switch (e.ListChangedType)
                    {
                        case ListChangedType.ItemMoved:
                            BeginSync();
                            this._source.Move(e.OldIndex, e.NewIndex);
                            EndSync(); 
                            break;

                        case ListChangedType.ItemAdded:
                            TModel modelAdded = _getWrappedModel(item);
                            BeginSync();
                            this._source.Add(modelAdded);
                            EndSync();
                            break;

                        case ListChangedType.ItemChanged:
                            //TModel modelChanged = _getWrappedModel(item);
                            //BeginSync();
                            //this._source[e.NewIndex] = modelChanged; 
                            //EndSync(); 
                            break;

                        case ListChangedType.Reset:
                            BeginSync();
                            this._source.Clear();
                            for (int i = 0; i < this.Count; i++)
                            {
                                TModel model = _getWrappedModel(this[i]);
                                this._source.Add(model);
                            }
                            EndSync();
                            break;
                    }
                }
            }
        }


        base.OnListChanged(e);
    }

    protected override void RemoveItem(int index)
    {
        if (!isSync) {
            TModel model = _getWrappedModel(this[index]);
            BeginSync();
            this._source.Remove(model);
            EndSync();
        }

        base.RemoveItem(index);
    }


    private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (isSync) return;

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                BeginSync();
                for (int i = 0; i < e.NewItems.Count; i++)
                {
                    this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                }
                EndSync();
                break;

            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1)
                {
                    BeginSync();
                    this.Swap(e.OldStartingIndex, e.NewStartingIndex);
                    EndSync();
                }
                else
                {
                    List<TViewModel> items = this.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToList();
                    BeginSync();
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAt(e.OldStartingIndex);

                    for (int i = 0; i < items.Count; i++)
                        this.Insert(e.NewStartingIndex + i, items[i]);
                    EndSync();
                }
                break;

            case NotifyCollectionChangedAction.Remove:
                BeginSync();
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);
                EndSync();
                break;


            case NotifyCollectionChangedAction.Replace:
                // remove
                BeginSync();
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);

                // add
                for (int i = 0; i < e.NewItems.Count; i++)
                    this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                EndSync();
                break;

            case NotifyCollectionChangedAction.Reset:
                BeginSync(); 
                Clear();
                for (int i = 0; i < e.NewItems.Count; i++)
                    this.Add(CreateViewModel((TModel)e.NewItems[i]));
                EndSync();
                break;

            default:
                break;
        }
    }



    public void Swap(int first, int second)
    {
        TViewModel temp = this[first];
        this[first] = this[second];
        this[second] = temp;
    }
}

您只需提供ViewModel类型,Model类型,Model工厂方法,ViewModel工厂方法以及指定如何从viewmodel对象获取包装模型的方法。 这是一个例子:

Func<Order> modelCreator = () => new Order();
Func<Order, OrderViewModel> viewModelCreator = (model => new OrderViewModel(model, _ctx));
Func<OrderViewModel, Order> modelGetter = (viewModel => viewModel._order);
var _viewModelCollection = new BindingViewModelList<OrderViewModel, Order>(_ctx.Orders.Local, viewModelCreator, modelCreator, modelGetter);

这不是一个相当简单的解决方案,但正如我所说,它运作良好,所以我标记这个线程作为回答。 希望有人能找到更好的解决方案..