Продолжим работу над WindowsFormsApplication, которое мы последний раз модифицировали в посте Знакомимся с бесплатной объектной базой db4o. Анализ.
Создадим новый файл Entities.cs. Разместим в нём описание двух сущностей — Country (её мы перенесём из Form1.cs) и Region. В этот же файл поместим описания интерфейсов ILinkable<T> и IDb4oClass, которые должны быть реализованы нашими сущностями. Туда же я поместил ещё два класса, на описании которых хотелось бы остановиться подробнее. В сущностях им не место, но я поленился создавать для них отдельные файлы. Как всё это будет выглядеть, можно увидеть здесь.
Поверхностный взгляд на ObjectContainerExtend может несколько удивить, ведь в прошлом посте мы говорили о том что сущности сами взаимодействуют с другими сущностями и управляют своим размещением в контейнере, а теперь приписываем контейнеру те же методы. На самом деле – это только первое впечатление. Контейнер играет роль селектора–универсализатора того, что делают сущности, хотя и не очень удачно. Наличие дублирующих методов SetObject и DeleteObject повышает риск внесения путаницы в Form1.cs и Analyse.cs. Лучше было бы реализовать интерфейс IObjectContainer в классе-обёртке, перегрузив методы Set и Delete и добавив Link и Unlink. Но это привело бы к обилию кода, отвлекающего от основной темы. Поэтому пришлось пойти на компромисс.
Теперь немного о классе EditList. В посте Знакомимся с бесплатной объектной базой db4o. Анализ, я говорил, что для стран каталог верхнего уровня не нужен, и мы использовали каталогизацию стран только для примера. Теперь, когда страны сами являются каталогами для регионов и эти связи управляются сущностями, реализующими методы ILinkable, наше понимание каталогизации поднимается на более высокий уровень. Поэтому каталогизация стран в качестве примера больше не нужна. Но что тогда будет источником данных для bindingSource? Хотелось бы иметь универсальный источник данных, в который можно было бы загружать как списки полученные методом Query, так и списки объектов-каталогов. EditList<T> является упрощённым прототипом такого источника. Упрощённым потому, что тема источников данных будет развиваться отдельно в посте посвящённом аспектам представления данных в контролах Windows.Forms, а сейчас нам нужно сосредоточиться на реализации сущностей. Теоретическую базу для этого мы подготовили в прошлом посте, поэтому я не комментирую описание сущностей, а сразу приступаю к модернизации нашего WindowsFormsApplication.
Первое, что нужно сделать, это заглянуть в Form1_Load:
IConfiguration config = Db4oFactory.Configure();
config.ObjectClass(typeof(Country)).CascadeOnUpdate(true);
config.ObjectClass(typeof(Region)).CascadeOnUpdate(true);
config.ObjectClass(typeof(List<Country>)).CascadeOnUpdate(true);
config.ObjectClass(typeof(List<Region>)).CascadeOnUpdate(true);
objectContainer = Db4oFactory.OpenFile(config, @"D:\Test.yap");
RefreshGrid1();
RefreshGrid2(CurrentCountry());
Как видите, в конфигураторе контейнера мы убрали каскадное удаление. Честно говоря, каскадное удаление объектов — опасная штука, не прощающая ошибок, но теперь, когда сущности сами управляют этим процессом, нам больше не о чем беспокоиться. Кроме того, наш метод RefreshGrid, переименован в RefreshGrid1, теперь у него такой вид:
bindingSource1.DataSource = new EditList<Country>(objectContainer.Query<Country>().ToList());
bindingSource1.ResetBindings(false);
Появился и RefreshGrid2 для грида регионов. Прежде, чем его описывать, создадим класс:
public class RegionSource : ListComponentTemplate<Region> { }
перейдём на Forms1.cs[Design] и добавим новые компоненты. Вначале добавьте на форму SplitContainer и сделайте его горизонтальным. bindingNavigator1 и dataGridView1 разместите в splitContainer1.Panel1. Как вы уже догадались, splitContainer1.Panel2 нужна для размещения bindingNavigator2 и dataGridView2. Кроме того, перетащите на форму BindingSource и RegionSource. regionSource1 свяжите с bindingSource2.DataSource. Добавьте на bindingNavigator2 три кнопки: toolStripButtonLoad2, toolStripButtonSave2 и toolStripButtonJoin. Свойство CheckState кнопки toolStripButtonJoin установите в Checked.
Теперь вспомним про RefreshGrid2 и CurrentCountry:
void RefreshGrid2(Country country)
{
if (country==null) bindingSource2.DataSource = new EditList<Region>(objectContainer.Query<Region>().ToList());
else bindingSource2.DataSource = new EditList<Region>(country);
bindingSource2.ResetBindings(false);
}
Country CurrentCountry()
{
if (!toolStripButtonJoin.Checked) return null;
return (Country)bindingSource1.Current;
}
Займёмся кнопками:
В toolStripButtonLoad_Click переименуйте RefreshGrid на RefreshGrid1. Событие toolStripButtonLoad2_Click будет таким:
RefreshGrid2(CurrentCountry());
В toolStripButtonSave_Click будут изменения:
dataGridView1.EndEdit();
bindingSource1.EndEdit();
((EditList<Country>)bindingSource1.DataSource).Set(objectContainer);
Аналогичным образом будет выглядеть toolStripButtonSave2_Click:
dataGridView2.EndEdit();
bindingSource2.EndEdit();
((EditList<Region>)bindingSource2.DataSource).Set(objectContainer);
И, наконец toolStripButtonJoin_Click будет выглядеть так:
toolStripButtonJoin.Checked = !toolStripButtonJoin.Checked;
RefreshGrid2(CurrentCountry());
Создайте событие bindingSource1_CurrentChanged и впишите туда:
if (!toolStripButtonJoin.Checked) return;
RefreshGrid2((Country)bindingSource1.Current);
Теперь пора вспомнить о форме Analyse.cs. Запускающее её событие toolStripButtonAnalyse_Click будет выглядеть так:
new Analyse(objectContainer).ShowDialog();
RefreshGrid1();
RefreshGrid2(CurrentCountry());
В самой форме, в toolStripButtonDelete_Click замените строку:
objectContainer.Delete(obj);
на
objectContainer.DeleteObject(obj);
Для начала хватит. Запустите проект на выполнение и посмотрите, что получилось. Выглядеть всё это должно приблизительно так:

Откройте анализ и удалите список стран, он нам больше не нужен. Вернитесь на Form1 и введите несколько регионов для разных стран (к примеру для Украины и России) не забывая нажимать на кнопку Save перед переходом на другую страну.
На первый взгляд похоже, что это Master-Slave. Но это не так. Между сущностями Country и Region установлены рефлекторные связи. У каждой страны есть свой список регионов (там где это введено), можете проверить в анализе. И каждый регион знает свою страну, что отображается в столбце Country. Пока не спешите прятать этот столбец в dataGridView2, лучше закрасьте его другим цветом, он нам ещё пригодится.
Это не множественная каталогизация, но для её организации нет никаких препятствий. Достаточно создать ещё какие-нибудь сущности, которые будут каталогами для регионов и завязать их с регионами через интерфейс Ilinkable<Region>. Другой вопрос, что здесь это не нужно, но мы можем сэмулировать воздействие множественной каталогизации из окна Анализ, как мы это делали в посте Знакомимся с бесплатной объектной базой db4o. Анализ. Мы удаляли объекты Country и получали "дырки" в каталогах, сетуя на отсутствие ссылочной целостности. Давайте повторим это сейчас для регионов. Выделите в анализе строки "Черкасская область" и "Хмельницкая область" и удалите эти объекты. Даже не выходя из анализа, видно, как отреагировал список Украины. Если бы нам потребовалось что-то вроде RDBMS-ного правила Restrict, то первую строчку метода Delete класса Region нужно было бы заменить на:
if (country != null) throw new Exception("Регион " + Name + " принадлежит стране " + country);
Попробуйте теперь удалить Донецкую область.
А как будут вести себя регионы, если мы удалим страну? Предугадать нетрудно, достаточно взглянуть на метод Delete класса Country. Сейчас там Set Null. Проверяем на Украине, удалим её из анализа. Как видим, страна и её список исчезли, но области остались. Попробуйте вариант Cascade на России.
И в заключении давайте обсудим кнопку Join, которая единственная из всех, находится в состоянии Checked. Нажмите на неё. Как видите bindingSource2.DataSource переключился с каталога регионов текущей страны на список всех регионов. Для нашей задачи это избыточно, но я включил эту возможность в демонстрационных целях, чтобы показать возможности EditList<T> и ещё раз подчеркнуть, что мы реализовали не классический Master—Slave, а объектную рефлекторную модель. Чтобы окончательно в этом убедиться, представите себе, что наши гриды находятся не на панелях splitcontainer1, а на отдельных формах, к примеру MDI. Представьте также, что кнопка Join в Unchecked состоянии присутствует на обоих bindingnavigator-рах, а в гридах не Country и Region, а Person и Phone. Люди являются каталогами для телефонов, а телефоны — каталоги для людей. Попеременно нажимая на Join-ы, мы можем подключаться либо к каталогам телефонов, либо к каталогам людей. Кто из них Master, а кто Slave? Возникает законный вопрос — а как организовать связывание сущностей, если Join-ы отключены? Один из вариантов мы сейчас рассмотрим на нашем примере. Для этого нам понадобится контекстное меню с двумя пунктами:
"Связать":
private void linkToolStripMenuItem_Click(object sender, EventArgs e)
{
if (toolStripButtonJoin.Checked) return;
Country country = bindingSource1.Current as Country;
if (country == null) { Console.Beep(); return; }
foreach (DataGridViewRow row in dataGridView2.SelectedRows) objectContainer.Link<Country>(((ILinkable<Country>)row.DataBoundItem), country);
}
и "Освободить":
private void unlinkToolStripMenuItem_Click(object sender, EventArgs e)
{
Country country = bindingSource1.Current as Country;
if (country == null) { Console.Beep(); return; }
foreach (DataGridViewRow row in dataGridView2.SelectedRows) objectContainer.Unlink<Country>(((ILinkable<Country>)row.DataBoundItem), country);
}
Для запуска меню используем событие:
private void dataGridView2_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex >= 0) return;
if (dataGridView2.SelectedRows.Count == 0) return;
contextMenuStrip1.Show(Cursor.Position);
}
Теперь, если вы находитесь в общем списке регионов, вы можете связать любые регионы с любой страной. Для этого выберите нужную страну в гриде стран, а в гриде регионов выделите строки тех регионов, которые нужно связать с выбранной страной. Контекстное меню запускается из заголовков выделенных строк.
На этом я завершаю тему сущностей. Но это не значит, что тема исчерпана, как таковая. Повторюсь, обсуждаемая здесь объектная модель, базирующаяся на рефлекторных связях сущностей — это один вариант из многих, используемых в OOBD. И если я скажу, что "Выбор модели зависит от поставленной задачи", это уже не будет подгонкой условий задачи под конкретное хранилище. Во всяком случае Db4o в этом плане ничем вас не ограничивает. Что построите, то контейнер и будет хранить.