我在复杂的网格数据绑定情况下第一次使用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)访问的信息。 我想要的是这样的事情:
在“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结果,因为:
由于这些问题,我试图以这种方式直接绑定POCO OrderClass:
this.OrderRepository.AsQueryable().Select()
将“间接”字段设置为“DataPropertyName”,如下所示:
列“名称” - > Customer.Name
列“城市” - > Customer.City
但同样,它不适合因为:
我的POCO类没有实现INotifyPropertyChanged,因此,如果我更改例如“ChCustomer”,它的相关数据(Name,City ec ..)将不会在屏幕上更新。
当我编辑ChCustomer时,显然数据绑定不知道如何根据用户插入的ChCustomer从其存储库中获取Customer实例,并将其分配给当前的Order“Customer”属性。
有时,可编辑字段(如ChCustomers)是复合键的一部分,实际上EF中不允许编辑实体的复合键。这引入了更多问题。
因此,在故事的最后,事实证明,当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。
对不起,如果我说话,但我尽力做到最清楚。 提前谢谢。
答案 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);
这不是一个相当简单的解决方案,但正如我所说,它运作良好,所以我标记这个线程作为回答。 希望有人能找到更好的解决方案..