Обновление данных при привязке данных с использованием привязки данных EF core и c#

#c# #winforms #data-binding #entity-framework-core

Вопрос:

Я использую VS 2019, приложение c# с формами Win и ядром Entity framework 5.0

В моем приложении у меня есть система.Windows.Формы.DataGridView используется для отображения и обновления данных в базе данных MySQL. Данные привязываются к DataGridView с помощью системы.Windows.Формы.Привязка источника myBindingSource и привязка данных таблицы EF с помощью

 myDbContext.SomeEntities.Load();
myBindingSource.DataSource = myDbContext.SomeEntities.Local.ToBindingList();
 

Это правильно отображает данные, как только я изменяю некоторые данные в таблице и вызываю MyDbContext.SaveChanges (), это сохраняет данные в базе данных.

Таким образом, пока приложение работает автономно, оно работает нормально.

Однако я хочу добиться того, чтобы форма, содержащая сетку, обновляла данные всякий раз, когда какое-либо другое действие за пределами моего приложения изменяет данные. Поэтому, если какое-либо обновление данных произойдет за пределами моего приложения, я хочу, чтобы эти изменения были немедленно видны в открытой форме без необходимости для пользователя закрывать и повторно открывать форму, содержащую представление DataGrid. Я знаю, что, конечно, мне нужен триггер для этих изменений. Это может быть таймер или внешний сигнал. На данный момент это таймер.

В таймере я делаю

 foreach( var rec in (BindingList<SomeEntities>)this.DataSource)
{
  DbContext.Entry(rec).Reload();
}
 

и после этого я делаю

 CurrencyManager cm = (CurrencyManager)((myDataGridView).BindingContext)[(ctrl as DataGridView).DataSource];
if (cm != null) cm.Refresh();
 

Это отлично работает для внешнего обновления существующей записи.
Однако, если запись вставлена или удалена, она завершается ошибкой. При внешней вставке новая запись просто не известна в существующем списке привязок и, следовательно, не обновляется; когда запись удалена извне, перезагрузка завершается неудачно (поскольку она больше не существует в базе данных).
И то, и другое достаточно понятно для того, что происходит.

Каков был бы правильный способ не только обновить существующие сущности, но и обновить содержимое коллекции MyDbContext.sometities

При поиске ответа я часто читаю «используйте короткий срок службы DbContext». Понятно, но мне нужен DbContext для возможности вызова MyDbContext.SaveChanges (), чтобы сохранить любые изменения, внесенные в сетку. Так ли это? Или есть другой способ? Если DbContext должен использоваться только во время загрузки сетки, как я могу использовать его в качестве источника данных для сетки, используя обычную привязку данных?

С EntityFramework 6 было

 _myObjectContext.RefreshAsync(RefreshMode.StoreWins, GetAll())
 

Не знаю, помогло бы это, так как я не пробовал использовать EntityFramework 6, но в EF core все равно нет эквивалента этому. Итак, есть ли какие-либо предложения?

Ответ №1:

Я думаю, что в таком сценарии — когда у вас постоянно открыта сетка данных (как я понял), это следует делать больше вручную.

Например, используя MVVM, я бы создал модель представления с некоторой наблюдаемой коллекцией элементов представления, например:

Предположим, что это ваша модель БД:

 public class DbItem
{
  public int Id {get;set;}
  public string Name {get;set;}
}
 

Теперь я хотел бы создать некоторые данные, которые будут использоваться в модели представления:

 public class ItemData: INotifyPropertyChanged
{

  public ItemData(DbItem item)
  {
     id = item.Id;
     name = item.Name; //notice that I use here backup field
  }
  public bool Modified {get; private set;}

  int id;
  public int Id 
  {
     get { return id; }
     set
     {
         if(id != value)
         {
            id = value;
            NotifyPropertyChanged();
         }
     }
  }

  string name;
  public string Name
  {
     get { return name; }
     set
     {
         if(name != value)
         {
            name = value;
            NotifyPropertyChanged();
         }
     }
  }
}
 

Здесь я использовал INotifyPropertyChanged, но все сводится скорее к вашим потребностям. Вы можете просто обновить поле Modified до значения true или каждый раз, когда запись изменяется, просто обновлять ее в БД (ОБНОВЛЕНИЕ/ВСТАВКА SQL).

Теперь, на мой взгляд, я бы сделал:

 public class ViewModel
{
  public ObservableCollection<ItemData> DataSource {get; set;}
}
 

Я сомневаюсь, что вы можете использовать ObservableCollection в WinForms так же, как в WPF, поэтому вместо этого вы могли бы создать некоторую коллекцию привязок, я думаю.

В любом случае, теперь, когда вы читаете данные из базы данных, вы должны преобразовать их в свои товары:

 public class ViewModel
{
  public void ReadData()
  {
     DataSource.Clear();
     List<DbItem> dbItems = service.GetDataFromDatabaseWithNoTracking();
     foreach(var item in dbItems)
     {
        DataSource.Add(new ItemData(item));
     }
  }
}
 

Теперь, когда вам нужно что-то обновить, тогда просто:

 public class ViewModel
{

  public void UpdateData(ItemData data)
  {
     //if data.Modified...
     DbItem db = new DbItem();
     db.Id = data.Id;
     db.Name = data.Name;
     service.UpdateItem(db);
  }

}
 

И в EF:

 public void UpdateItem(DbItem item)
{
  var entry = dbContext.Entry(item);
  dbContext.Save();
}
 

Все сводится к тому, что вы не будете отслеживать записи из базы данных. Вы должны сделать это вручную.

Что вы думаете об этом решении?

Комментарии:

1. Ну, на самом деле это то, из чего я исходил, когда начал переходить на entity framework. В предшественнике нового приложения я сделал это превосходно, имея все данные под ручным управлением. (В старой программе для чтения и изменения данных использовались прямые команды SQL). В моем эквиваленте ваших данных чтения() я даже не очистил и не воссоздал элементы, вместо этого я обновил существующие. Все это большая куча кода, который нужно было поддерживать. Поэтому, когда я начал создавать его заново, я подумал, что entity framework должна избавить меня от этого ручного кодирования, похоже, я ошибался.

2. Ну, ваши потребности более конкретны, чем то, что есть у EF. Так что тебе придется еще немного поработать.

Ответ №2:

Я не уверен, что это будет хороший интерфейс. Предположим, оператор счастливо редактирует строку, и вдруг вы решаете перезагрузить таблицу: все его изменения потеряны? И что, если оператор просто изменил значение с 4 на 5, в то время как кто-то другой просто изменил то же значение с 4 на 3, какое из них мы должны сохранить? А что, если кто-то другой решит удалить строку, потому что он подумал

Поэтому я бы рекомендовал не делать автоматическое обновление, добавьте для этого кнопку. Что — то вроде кнопки перезагрузки в браузере. Добавьте кнопку F5, чтобы начать обновление, и ваш интерфейс совместим с большинством приложений Windows, которые могут отображать устаревшие данные.

Однако это имеет тот недостаток, что, если оператор отредактировал значение, которое кто-то другой также отредактировал или даже удалил, вы должны решить, что делать. Мой совет был бы таков: спросите оператора:

  • Если оператор отредактировал строку, которую также отредактировал кто-то другой: какую строку мы должны сохранить?
  • Если оператор отредактировал строку, которую удалил кто-то другой?
  • Если оператор удалил строку, которую только что изменил кто-то другой?

Давайте предположим, что ваш datagridview показывает Products :

 class Product
{
    public int Id {get; set;}
    public string Name {get; set;}
    public decimal Price {get; set;}
    public int Stock {get; set;}
    ...
}
 

Используя конструктор Visual studio, вы добавили представление DataGrid и некоторые столбцы. В своем конструкторе вы можете назначить свойства столбцам:

 public MyForm() : Form
{
    InitializeComponents()

    this.columnId.DataPropertyName = nameof(Product.Id);
    this.columnName.DataPropertyName = nameof(Product.Name);
    this.columnPrice.DataPropertyName = nameof(Product.Price);
    ...
}
 

Вам также нужен метод для извлечения продуктов, которые должны отображаться в базе данных, а затем их повторной выборки, чтобы увидеть, какие продукты были изменены. Конечно, вы скрываете, что они из базы данных.

 IEnumerable<Product> FetchProductsToDisplay()
{
    ... // Fetch the data from the database; out-of-scope of this question
}
 

To display the fetched Products, use the DataSource and a BindingList:

 BindingList<Product> DisplayedProducts
{
    get => (BindingList<Product>)this.dataGridView1.DataSource;
    set => this.dataGridView1.DataSource = value;
}
 

Сначала вы показываете продукты:

 void OnFormLoading(object sender, ...)
{
    this.DisplayedProducts = this.FetchProductsToDisplay()
}
 

Так что теперь оператор с радостью может редактировать существующие продукты, возможно, добавлять некоторые новые или удалять некоторые продукты. Через некоторое время он хочет обновить продукты с помощью данных, введенных другими:

 private void OnButtonRefresh_Clicked(object sender, ...)
{
    this.UpdateProducts();
}

private void UpdateProducts()
{
    IEnumerable<Product> dbProducts = this.FetchProductsToDisplay();
    IEnumerable<Product> editedProducts = this.DisplayedProducts();
 

Мы должны сравнить Продукты из базы данных с продуктами в представлении DataGrid. Сопоставьте продукты по идентификатору: тот же идентификатор, ожидайте тот же продукт.

  • Идентификатор равен нулю, и, следовательно, только в редактируемых продуктах он был добавлен оператором, но еще не добавлен в базу данных.
  • ненулевой идентификатор есть только в редактируемых продуктах: он был удален кем-то другим
  • Идентификатор есть как в dbProducts, так и в editeProducts, но значения не равны: либо редактируется оператором, либо кем-то другим

Трудный вопрос:

  • Идентификатор есть только в dbProducts: он был добавлен кем-то другим или удален оператором.

Чтобы иметь возможность задать оператору правильный вопрос, похоже, нам также нужны продукты, которые были показаны до того, как оператор начал редактировать.

 private IEnumerable<Product> OriginalProducts => ...
 

Таким образом, теперь мы можем определить, какие продукты добавлены / удалены / изменены оператором и/или в базе данных:

 Id in originalProducts editedProducts dbProducts
          yes              yes          yes     compare values to detect edits
          yes              yes          no      someone else deleted
          yes              no           yes     operator deleted.
          yes              no           no      both operator and someone else deleted

      no               no            yes       someone else added
      no               yes           no        operator added
      no               yes           yes       both operator and someone else added
 

Процедура обнаружения изменений:

 private void DetectChanges(IEnumerable<Product> originalProducts,
                           IEnumerable<Product> editedProducts,
                           IEnumerable<Product> dbProducts)

{
    // do a full outer join on these three sequences:
    var originalDictionary = originalProducts.ToDictionary(product => product.Id);
    var dbDictionary = dbProducts.ToDictionary(product => product.Id);

    // some Ids in editedProducts have value zero:
    var addedProducts = editedProducts.Where(product => product.Id == 0);
    var editedDictionary = editedProducts.Where(product => product.Id != 0)
        .ToDictionary(product => product.Id);

    var allUsedIds = originalDictionary.Keys
        .Concat(dbDictionary.Keys)
        .Distinct();
 

Примечание: все отредактированные продукты с идентификатором != 0 уже существовали в базе данных, когда они были извлечены в последний раз, поэтому их идентификаторы уже находятся в разделе originalDictionary. Я использовал Distinct для удаления дубликатов идентификаторов

     foreach (int id in allUsedIds)
    {
        bool idInDb = dbDictionary.TryGetValue(id, out Product dbValue)
        bool idInOriginal = originalDictionary.TryGetValue(id, out Product originalValue);
        bool idInEdited = editedDictionary.TryGetValue(id, out Product editedValue);
 

Используйте таблицу выше и значения idInDb / idInOriginal / idInEdited, чтобы узнать, было ли добавлено или удалено / изменено значение.

Иногда бывает достаточно просто добавить / отредактировать / Изменить значение в представлении сетки данных; иногда вам придется спросить оператора.

Совет:

Изменения, внесенные кем-то другим:

  • Если его добавил кто-то другой: просто добавьте его в представление DataGridView
  • Если удалено кем-то другим, а не отредактировано оператором: просто удалите из DataGridView
  • Если удалено кем-то другим, отредактировано оператором: спросите оператора, что делать

Изменения, внесенные оператором:

  • Если добавлено оператором (Id == 0): вставить в базу данных
  • Если оператор удалит его, удалите его из базы данных
  • Если редактируется оператором, а не кем-то другим: обновите базу данных
  • Изменения, внесенные оператором и кем-то другим: спросите оператора, какой из них сохранить. Либо обновите базу данных, либо представление DataGrid.

Detect Product Changes: use IEqualityComparer

Чтобы обнаружить изменение, вам нужен компаратор равенства, который сравнивает по значению:

 public class ProductComparer : EqualityComparer<Product>
{
    public static IEqualityComparer<Product> ByValue {get} = new ProductComparer();

    public override bool Equals(Product x, Product y)
    {
        if (x == null) return y == null;              // true if both null
        if (y == null) return false;                  // because x not null
        if (Object.ReferenceEquals(x, y) return true; // same object
        if (x.GetType() != y.GetType()) return false; // different types

        return x.Id == y.Id
            amp;amp; x.Name == y.Name   // or use StringComparer.CurrentCultureIgnoreCase
            amp;amp; x.Price == y.Price
            amp;amp; ...
    }

    public override int GetHashCode(Product x)
    {
        if (x == null) return 87742398;

        // for fast hash code: use Id only.
        // almost all products are not edited, so with same Id have same values
        return x.Id.GetHashCode();
    }
}
 

Использование:
Iequalityкомпаратор productValueComparer = Сопоставитель продукта.Бывало;

После определения того, какие продукты находятся в каком словаре:

 if (idInDb amp;amp; idInEdited amp;amp; )
{
    // one of the Products in the DataGridView already exists in the database
    // did the operator edit it?

    bool operatorChange = !productValueComparer.Equal(originalValue, editedValue);
    bool dbChange = !productValueComparer.Equal(originalValue, dbValue);

    if (!productValueComparer.Equal(operatorValue, dbValue);)
    {
        // operator edited the Product; someone change it in the database
        // ask operator which value to keep
        ...
    }
    else
    {
        // operator edited the Product; the same value is already in the database
        // nothing to do.
    }
 

Поэтому для каждого идентификатора проверьте, был ли он уже в базе данных, проверьте, все ли он еще в базе данных, и проверьте, сохранил ли оператор его в представлении DataGrid. Также проверьте, какие значения изменены, чтобы определить, нужно ли вам добавлять / удалять / обновлять базу данных или представление DataGrid, или, возможно, вам ничего не нужно делать.