Навигация по диалогам - QualitySolution/QSProjects GitHub Wiki
Механизм навигации по диалогам и сами диалоги тесно связанны. Мы реализуем паттерн MVVM.
Каждый диалог должен состоять минимум из двух(чаще 3-х) классов(частей кода):
- View - непосредственно описание, как диалог выглядит, эта часть полностью зависит от используемого тулкита. В ней не должно быть кода, кроме связывания свойств ViewModel с виджетами и контролами формы. Более менее программный код разрешается только в случае необходимости какой-то особой поддержки работы с со специфичными особенностями GUI.
- ViewModel - здесь должна располагаться вся логика диалога, то есть весь тот код, который обрабатывает действия пользователя, а также формирует данные, которые необходимо пользователю показать в том или ином виде.
- Model - самое размытое понятие во всем патерне. Так как это часто группа или совокупность классов, которые реализуют непосредственно бизнес логику работы с данными. Обычно это как минимум классы доменной модели, с которыми работает диалог, которые потом сохраняются в базу. Но в это понятие должны включатся также все классы, которые позволяют каким-то образом обрабатывать данные приложения, при этом код обработки этих данные не связан с одним конкретным диалогом, а вполне успешно может использоваться множеством диалогов.
Различным классам кода надо иметь возможность открывать новые диалоги, давать команды на закрытие, и другими способами взаимодействовать с диалогами. Для того, чтобы остальной код приложения ничего не знал про используемый GUI, но мог открыть новый диалог он должен обращаться к NagigationManager
, который как раз организует работу с диалогами и посредством интерфейса INavigationManager
и идущих с ним скрывает от бизнес кода конкретную реализацию отображения диалогов.
На текущий момент готово две реализации NagigationManager:
- TdiNavigationManager - Менеджер навигации открывающий вкладки на базе старого Tdi вкладочного интерфейса для Gtk. В первую очередь, рассчитан на совместную работу и старого вкладочного интерфейса и новых диалогов, построенных на ViewModel, в режиме совместимости также умеет отрывать через себя обычные Tdi вкладки. Возможно после полного перехода приложении на ViewModel-и можно будет использовать другой, более простой внутри менеджер навигации, который не будет отягощен поддержкой работы с TDI.
- GtkWindowsNavigationManager - Более простая реализация менеджера навигации, открывающая каждый диалог в своем окне Gtk.
NavigationManager обычно создается один на все приложение, чтобы он мог управлять всеми открытыми на текущий момент диалогами. Код диалогов обычно не должен иметь глобальную ссылку на NavigationManager, так как в базовом классе DialogViewModelBase, от которого наследуется большинство ViewModel менеджер необходимо передавать в конструктор, что у всех диалогов, он по умолчанию есть и они могут открывать другие диалоги, обращаясь к свойству NavigationManager в базовом классе.
Открыть новую ViewModel можно вызовом различных методов OpenViewModel. Все они создают и открывают ViewModel c соответствующей для них View, с различными настройками:
- TViewModel - тип класса ViewModel-и который необходимо создать.
- DialogViewModelBase master - мастер ViewModel в случае открытия подчиненной ViewModel, это поле обязательно для заполнения, так как создаваемая ViewModel будет считаться подчиненной. Если же режим открытия обычный, то новая вкладка просто откроется за мастер вкладкой.
- OpenPageOptions - флаги открытия, могут использоваться совместно.
- AsSlave - вкладка должна открыться в виде подчиненной, это означает что ее отдельное существование без основной вкладки бессмысленно. Например, она открывалась для того, чтобы пользователь выбрал что-то в журнале и результат вернулся в основной диалог.
- IgnoreHash - Игнорировать Hash при открытии, читай раздел про Hash-и страниц.
- addingRegistrations - опционально возможность передать метод, выполняющий дополнительные регистрации в scope вкладке, создаваемой через Autofac, это позволяет переопределить регистрацию каких-либо классов в скопе, создаваемой для новой вкладки, смотри раздел про Autofac. Вам в создаваемую ViewModel может понадобится передать какие-либо параметры. Их можно передавать несколькими способами:
- Типизированными параметром, например если мы в во ViewModel хотим передать два аргумента int и string, мы должны совершить следующий вызов
OpenViewModel<MyViewModel, int, string>(masterViewModel, 5, "текст")
. Этот способ естественным образом ограничен количеством аргументов в сигнатурах, добавленных в интерфейс. - Передачей двух массивов с типами параметров и их значениями. Предыдущий пример выполняется с следующим кодом
OpenViewModelTypedArgs<MyViewModel>(masterViewModel, new Type[]{typeOf(int), typeOf(string)}, new object[]{ 5, "текст"})
- Передачей именованных параметров. То есть, Вы передаете не тип параметра в конструкторе его имя, чтобы библиотека могла найти нужную сигнатуру конструктора. Похожий с предыдущими примерами можно выполнить так
OpenViewModelNamedArgs<MyViewModel>(masterViewModel, new Dictionary<string, object> {{"id", 5}, {"message", "текст"}})
Все методы открытия ViewModel возвращают класс, реализующий интерфейс IPage, то есть это дополнительная внешняя информация от открытой ViewModel, которой для внутреннего использования оперирует система навигации. В каждом конкретном менеджере навигации в этом классе может хранится разная информация, например для TdiNavigationManager там дополнительно хранится ссылка на TdiTab. А для GtkWindowsNavigationManager, хранятся ссылки на окно Gtk, в котором отображается View.
Но все страницы также реализуют некоторые общие свойства, пользовательский код, создавший страницу может получить ее ViewModel, подписаться на события закрытия, получить список подчиненных страниц, и внутренних(дочерних) ViewModel.
Обратите внимание, что менеджер навигации может работать с разными типами страниц, например TdiNavigationManager для открытия модальных ViewModel, то есть открытых в отдельном окне, использует в качестве страницы GtkWindowPage
Обратите внимание, что при использовании фабрик ViewModel, завязанных на AutoFac, при передачи типизированных параметров, порядок параметров не важен, важен только тип самого параметра. AutoFac ищет самый подходящий тип конструктора, параметры которого по типам он может определить, через переданные вручную, а также из своих зарегистрированных классов.
Это означает, что используя типизированные параметры, невозможно например передать два разные параметра одного типа. Например, если в конструкторе будет указаны аргументы (int x, int y)
, то передавая несколько параметров int вы получите странный результат, все параметры int будут заполнены одним значением. Если нужно передавать в конструктор параметры одного типа, Вы должны использовать именованную передачу параметров.
Все текущие реализации фабрик ViewModel используют AutoFac для создания экземпляров. При этом для каждой странице они создают новый Lifetime Scope. Это означает, что для закрытия ресурсов, помимо реализации интерфейса IDisposable и ручного закрытия всех ресурсов в методе Dispose()
, можно использовать жизненный цикл ViewModel, то есть все классы созданные через AutoFac внутри вкладки и для вкладки(передачей в конструктор), будут автоматически убиты вместе со ViewModel при закрытии.
Если внутри ViewModel необходимо через AutoFac создавать экземпляры классов, можно просто в конструктор ViewModel добавить аргумент ILifetimeScope и он автоматически заполнится текущим скопом, внутри которого создается вкладка.
Для того чтобы пользователь не открывал несколько диалогов редактирования для одного и того же документа(объекта) или несколько однотипных журналов, в систему навигации добавлено такое понятие как hash-диалога. То есть, некая мета-информация, строка hash, позволяющая системе навигации при попытке открыть диалог повторно, вместо открытия копии просто переключить пользователя на уже открытый диалог. Используется именно строка, так как система навигации должна еще до создания ViewModel понять, что аналогичная ViewModel уже открыта. То есть, должен иметься механизм создания какого-то идентификатора ViewModel еще до ее создания, при этом допустим ViewModel редактирующая клиента с id 1 должна отличатся от ViewModel редактирующая клиента с id2.
На текущий момент имеется только одна реализация генератора Hash, базирующаяся на имени класса и специфичных параметров конструктора ClassNamesHashGenerator. Но легко можно выполнить свою реализацию. Данная реализация принимает в конструкторе перечисление IExtraPageHashGenerator, которое позволяет дополнять поведение генератора. В этом случае генератор сначала будет пытаться получить хеш от дополнительных генераторов, если не один из них не сработает hash будет создан основным способом. Такая реализация была добавлена для возможность размещать дополнительное поведение зависимое от классов проекта или необязательных библиотек.
В библиотеке предложен готовый вариант генератора хеша диалога, позволяющий учитывать любые из параметров переданных в конструктор диалога. Пример настройки:
var parametersHashGenerator = new WithParametersHashGenerator()
//Здесь конфигурируем диалог которому передается id объекта в виде int, каждый вызов диалога со свои id считается отдельным диалогом.
.Configure<HistoryNotificationViewModel>().AddParameter<int>(id => id.ToString())
//Здесь конфигурируем диалог которому передается сам объекта сотрудника EmployeeCard, в хеше используется id сотрудника вызов диалога со свои id считается отдельным диалогом.
.Configure<SpecCoinsOperationsJournalViewModel>().AddParameter<EmployeeCard>(emp => emp.Id.ToString())
.End();
В этот генератор можно как передавать несколько раз одну и туже ViewModel но с разным набором параметров. Каждый раз можно указывать не сколько параметров, будет использована та настройка в которой совпадут все настроенные параметры. Если хотя бы одного типа параметра не будет указано в конструкторе этот вариант настройки не сработает.
Для новой сущности
NavigationManager.OpenViewModel<TEntityViewModel, IEntityUoWBuilder>(this, EntityUoWBuilder.ForCreate());
Для изменение существующей, с указанием id.
NavigationManager.OpenViewModel<TEntityViewModel, IEntityUoWBuilder>(this, EntityUoWBuilder.ForOpen(Id));
Пример ниже используется обычно в главном окне, здесь не указана вкладка после которой надо открыть журнал.
NavigationManager.OpenViewModel<NomenclatureJournalViewModel>(null);
Открытие журнала для выбора объекта, внутри другого диалога.
var selectPage = NavigationManager.OpenViewModel<NomenclatureJournalViewModel>(this, OpenPageOptions.AsSlave);
selectPage.ViewModel.SelectionMode = QS.Project.Journal.JournalSelectionMode.Multiple;
selectPage.ViewModel.OnSelectResult += Nomeclature_OnSelectResult;
- this - текущий диалог.
- OpenPageOptions.AsSlave - открытие в виде подчиненной вкладки. Главную вкладку нельзя будет закрыть без закрытия подчиненной.
- JournalSelectionMode.Multiple - режим выбора объекта. В данном случае пользователю позволяется вбирать сразу несколько объектов через Ctrl или Shift
- Nomeclature_OnSelectResult - метод обработчик события возникающего после выбора пользователем в журнале.
var reportInfo = new ReportInfo {
Title = String.Format("Документ №{0}", Entity.Id),
Identifier = "MyDoc",
Parameters = new Dictionary<string, object> {
{ "id", Entity.Id }
}
};
NavigationManager.OpenViewModel<RdlViewerViewModel, ReportInfo>(this, reportInfo);
Основная статья: Печатные формы (отчеты)
Этот интерфейс специально создан на переходный период, позволяющий работать с проектами у которых есть старые Tdi диалоги и новые ViewModel и есть необходимость вызывать из новых диалогов старые и наоборот.
Интерфейс ITdiCompatibilityNavigation
специально выделен отдельно, чтобы можно было четко понимать где еще используется старые диалоги. Его реализует только TdiNavigationManager
.
var mainPage = navigation.OpenTdiTab<EmptyDlg>(null);
Открытие TDI диалога EmptyDlg
без мастер вкладки.
var selectPage = tdiNavigationManager.OpenTdiTab<OrmReference, Type>(masterViewModel, typeof(Nomenclature), OpenPageOptions.AsSlave);
Здесь мы говорим что в конструктор OrmReference нужно передать параметр типа Type
который будет иметь значение typeof(Nomenclature)
.
var selectPage = tdiNavigationManager.OpenTdiTab<ReferenceRepresentation>(
masterViewModel,
OpenPageOptions.AsSlave,
c => c.RegisterType<EmployeesVM>().As<IRepresentationModel>()
);
Тут мы добавляем в регистрации Autofac только для скопа создаваемой вкладки, класс EmployeesVM
и говорим что он реализует интерфейс IRepresentationModel
. Виджет ReferenceRepresentation
в конструкторе имеет аргумент IRepresentationModel
, поэтому AutoFac автоматически создаст нам нужный EmployeesVM
.
Для работы внутри диалогов TDI в интерфейсе ITdiCompatibilityNavigation
есть аналоги выше описанных методов с окончанием ...OnTdi
. Эти методы позволяют использовать менеджера навигации внутри старого диалога TDI. Так как во все выше указанные методы в качестве мастер ViewModel нужно передавать текущую ViewModel, но в диалоге Tdi ее просто нет. Поэтому в эти методы нужно передавать текущую Tdi вкладку.
Пример ниже показывает открытие журнала ViewModel внутри диалога TDI
var selectJournal = MainClass.MainWin.NavigationManager.OpenViewModelOnTdi<NomenclatureJournalViewModel>(MyTdiDialog, QS.Navigation.OpenPageOptions.AsSlave);
selectJournal.ViewModel.SelectionMode = QS.Project.Journal.JournalSelectionMode.Multiple;
selectJournal.ViewModel.OnSelectResult += AddNomenclature_OnSelectResult;
Таким же способом через навигатор можно открыть диалог TDI внутри диалога TDI используя метод OpenTdiTabOnTdi
. Он имеет смысл для начала переписывания старых диалогов на новый поход.
Основные пространства имен для работы с диалогами:
- QS.Navigation - Классы для управления диалогами NavigationManager и сопутствующие классы.
- QS.ViewModels - Классы для работы с ViewModel-ями.
- QS.ViewModels.Control - Базовые ViewModel-и контролов или виджетов.
- QS.ViewModels.Dialog - Базовые ViewModel-и диалогов.
- QS.ViewModels.Extension - Интерфейсы расширения для ViewModel-е, позволяющие им реализовывать дополнительный функционал.
- QS.Views.Control - View контролов.
- QS.Views.Dialog - Базовые View диалогов.
- QS.Views.Resolve - Модуль позволяющий создать соответствующий View для ViewModel.
Ниже предлагается пример базовой настройки проекта для работы TdiNavigationManager, все основные классы которые необходимо зарегистрировать в AutoFac.
var builder = new ContainerBuilder();
#region Навигация
builder.RegisterType<ClassNamesHashGenerator>().As<IPageHashGenerator>();
builder.Register((ctx) => new AutofacViewModelsTdiPageFactory(AppDIContainer)).As<IViewModelsPageFactory>();
builder.Register((ctx) => new AutofacTdiPageFactory(AppDIContainer)).As<ITdiPageFactory>();
builder.Register((ctx) => new AutofacViewModelsGtkPageFactory(AppDIContainer)).AsSelf();
builder.RegisterType<TdiNavigationManager>().AsSelf().As<INavigationManager>().As<ITdiCompatibilityNavigation>().SingleInstance();
builder.RegisterType<BasedOnNameTDIResolver>().As<ITDIWidgetResolver>();
builder.Register(cc => new ClassNamesBaseGtkViewResolver(typeof(RdlViewerView), typeof(OrganizationView))).As<IGtkViewResolver>();
#endregion
#region ViewModels
builder.RegisterAssemblyTypes(System.Reflection.Assembly.GetAssembly(typeof(OrganizationViewModel)))
.Where(t => t.IsAssignableTo<ViewModelBase>() && t.Name.EndsWith("ViewModel"))
.AsSelf();
#endregion
AppDIContainer = builder.Build();
Внутренние механизмы менеджера навигации в какой-то момент должны иметь возможность по ViewModel-и создать соответствующий View.
Для поиска(резольва) view для Gtk используется интерфейс QS.Views.Resolve.IGtkViewResolver из библиотеки QS.Project.Gtk. Он имеет всего 1 метод получения Gtk.Widget для переданной ViewModel. Его легко реализовать самостоятельно. Но лучше использовать или несколько стандартных резольверов из библиотеки. Их объединение реализовано с помощью патерна Декоратор, когда они вкладываются друг в друга как матрешки при создании. И если резолвер более высокого уровня не может найти View, запрос передается на резолвер находящийся внутри.
- RegisteredGtkViewResolver - Позволяет вручную связывать каждую viewModel с конкретной View
- ClassNamesBaseGtkViewResolver - Автоматически находит View исходя и названия и пространства имен ViewModel-и по следующей схеме: {CoreNamespace}.ViewModels.{SubNamespaces}.{Name}ViewModel - для модели представления; {CoreNamespace}.Views.{SubNamespaces}.{Name}View - для gtk представления; В конструктор получает список Assembly, в которых необходимо проводить поиск.