При разработке приложений часто встречается следующий сценарий: имеется некоторый набор данных доступных для просмотра и редактирования, например, это могут быть бизнес-сущности или настройки приложения. В момент, когда пользователь решает что-либо отредактировать, ему обычно становится доступна специальная форма с нужными полями ввода-вывода и другими элементами управления. Если он вносит какие-либо корректировки в данные, то при обработке формы хорошим тоном является запрос-подтверждение перед окончательным применением внесённых правок. В случае согласия пользователя данные обновляются в источнике и на интерфейсе, а при отмене используются старые значения.
Данная задача включает две подзадачи:
1) когда пользователь уходит с формы редактирования, необходимо понимать, действительно ли он произвёл изменения, чтобы не задавать вопрос на подтверждение впустую и не перезаписывать идентичные данные;
2) если редактированию подвергается непосредственно исходная сущность, а не её копия, то в случае отмены необходимо сохранять возможность отката к исходным значениям.
В статье мы рассмотрим обобщённый и очень лаконичный [размером в несколько строк кода!] подход к решению подобного рода задач, основанный на использовании библиотеки Replication Framework.
Рассмотрим пример приложения. Пусть дан список сущностей, среди которых пользователь может выбрать любую и нажать на кнопку редактирования [в режиме оригинала либо копии].
В режиме редактирования оригинала при изменении сущности в диалоговом окне соответствующие значения немедленно обновляются и в главном, чего не происходит в режиме копии.
После подтверждения изменений выводится список всех найденных различий, если поднят флаг Show detailed changes, либо просто выводится сообщение об обнаружении хотя бы одного отличия [в реальных ситуациях иногда достаточно и такого поведения].
При отмене используются старые значения.
Теперь взглянем на код метода, который отвечает за данное поведение.
private void Edit<T>(T sourceEntry, bool useCopy, bool showChanges, ReplicationProfile replicationProfile)
{
var cache = new ReconstructionCache();
var sourceSnapshot = sourceEntry.CreateSnapshot(cache, replicationProfile);
var editableEntry = useCopy ? sourceSnapshot.ReplicateGraph() : sourceEntry;
if (GetView(editableEntry).ShowDialog() == true)
{
var resultSnapshot = editableEntry.CreateSnapshot(null, replicationProfile);
var changes = sourceSnapshot.Juxtapose(resultSnapshot)
.Where(j => j.State != Etalon.State.Identical);
if (changes.Any())
{
MessageBox.Show(showChanges
? changes.Aggregate("", (x, y) => x + y + Environment.NewLine)
: "Any changes has been detected!");
UpdateSourceData(editableEntry);
UpdateUserInterface();
}
else MessageBox.Show("There are no any changes.");
}
else if (!useCopy) sourceSnapshot.ReconstructGraph(cache);
}
public class Person : INotifyPropertyChanged
{
private int _id;
private string _name;
private string _birthday;
private string _phone;
private string _mail;
public event PropertyChangedEventHandler PropertyChanged = (o, e) => { };
private void Set<T>(ref T target, T value, [CallerMemberName]string caller = "")
{
if (Equals(target, value)) return;
target = value;
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
public int Id
{
get => _id;
set => Set(ref _id, value);
}
public string Name
{
get => _name;
set => Set(ref _name, value);
}
public string Birthday
{
get => _birthday;
set => Set(ref _birthday, value);
}
public string Phone
{
get => _phone;
set => Set(ref _phone, value);
}
public string Mail
{
get => _mail;
set => Set(ref _mail, value);
}
}
private static readonly ReplicationProfile PersonRepicationProfile = new ReplicationProfile
{
MemberProviders = new List<MemberProvider>
{
new CoreMemberProviderForKeyValuePair(),
new CoreMemberProvider(BindingFlags.Public | BindingFlags.Instance, Member.CanReadWrite),
}
};
Как видим, метод достаточно обобщённый и его можно испольовать для сущностей других типов.
Теперь обратим внимание на ключевые моменты. Работа библиотеки Replication Framework основана на использовании мгновенных снимков объектов в произвольные моменты времени, то есть с помощью метода-расширения Snapshot можно запросто сделать хронику мутаций [историю изменений] произвольного объекта или графа.
var cache = new ReconstructionCache();
var sourceSnapshot = sourceEntry.CreateSnapshot(cache, replicationProfile);
...
var resultSnapshot = editableEntry.CreateSnapshot(null, replicationProfile);
Далее можно сопостить два снимка, чтобы выявить различия в состоянии графа между двумя любыми контрольными точками.
var changes = sourceSnapshot.Juxtapose(resultSnapshot)
.Where(j => j.State != Etalon.State.Identical);
С помощью вызова метода ReplicateGraph можно воссоздать новую копию графа идентичную той, что зафиксирована на снимке, а с помощью ReconstructGraph при наличии кэша репликации совершить реконструкцию графа, то есть вернуть старый экземпляр к прежнему состоянию.
var editableEntry = useCopy ? sourceSnapshot.ReplicateGraph() : sourceEntry;
var cache = new ReconstructionCache();
var sourceSnapshot = sourceEntry.CreateSnapshot(cache, replicationProfile);
...
else if (!useCopy) sourceSnapshot.ReconstructGraph(cache);
Более подробную информацию об использовании библиотеки вы можете найти в предыдущих публикациях:
1) Replication Framework • глубинное копирование и обобщённое сравнение связных графов объектов
2) Обобщённое копирование связных графов объектов в C# и нюансы их сериализации
Библиотека является бесплатной для некоммерческих и учебных проектов, а на Nuget доступна пробная версия, которая функциональна до конца лета. Для получениея лицензионной версии с неограниченным сроком действия и доступа к исходным кодам необходимо отправить запрос на данный адрес.
За внешней простотой использования и хорошей функциональностью библиотеки кроется большая и кропотливая работа по её созданию и отладке, поэтому любая материальная поддержка и покупка коммерческой лицензии очень приветсвуются!
Вдохновения тебе, читатель!
Автор: Makeman