Всем привет! Сегодня рассмотрим интересный функционал, который даёт нам библиотека Community.Toolkit — обмен сообщениями.
Задача
Предположим, у нас есть некое приложение, в котором есть модуль, который мы разделили на маленькие классы, что-то типа такого:
В данном случае классы, находящиеся выше, знают, что они будут использовать нижележащие классы, но нижележащие классы универсальны и не знают, кто и как их будет использовать. Но тут мы хотя бы в пределах одного проекта, и мы можем прокинуть эти зависимости в обратном направлении. Но, может быть, у нас вообще сложное приложение из нескольких модулей:
Модули знают о существовании Common API, но не знают о проекте Revit App, который их использует. Что если нам надо передать информацию из модуля 1 в модуль 3? Добавить связь между ними? А если в обратном направлении, связь между проектами всегда односторонняя, что делать?
Вариант решения
Это задача сложная и интересная, поэтому решений может быть множество. Я покажу вам одно из них — через Messenger в Community.Toolkit.Mvvm (или, как говорит автоперевод Майкрософта, Посланник)
Наиболее частый сценарий такой: мы создаём класс, который становится приёмником сообщений. Он слушает все сообщения из всех мест, и, если сообщение приходит, выполняет код, написанный в методе Receive.
В целом, ещё есть сценарии запроса сообщений и ответа на сообщения, но мы здесь их рассматривать не будем, чтобы не утяжелять статью. Подробнее можете почитать в Samples или установить Sample App там же и поиграться.
Реализуем такой сценарий:
1. Возьмём проект c DockPanel отсюда
2. Добавим туда команду, которая при нажатии будет отправлять сообщение с Id выделенных элементов
3. Выведем в DockPanel текст этого сообщения
И да, самое важное — команда и панель не будут ничего знать друг о друге.
Реализация
Установим nuget-пакет Community.Toolkit, если у нас его нет (в шаблоне Nice3Point он включен по умолчанию, но в этом проекте нет):
Напишем класс сообщения, что именно мы будем отправлять. Контент может быть любым, необязательно строка, всё что угодно, хоть даже ничего — тогда сообщение будет просто сигналом на запуск действия. В данном случае я хочу передать выбранные Id, передам их сразу в виде отформатированной строки:
Напишем команду, которая отправляет сообщения. Я использовал IExternalCommand, в целом это может быть любая команда из любого окна или любого места приложения, любой метод, из которого вы хотите что-то сообщить.
Обратите внимание, как по мне, я довольно элегантно отформатировал строку, чтобы вывести Id через запятую
Теперь самый важный момент — регистрируем приём сообщений в нужном классе. В моём примере я буду регистрировать прямо в странице, которую я отображаю, но обычно это делается во ViewModel. Обратите внимание на жизненный цикл объекта, который принимает сообщение: как только объект перестаёт существовать, вам нужно отписаться от приёма сообщений, иначе сборщик мусора не удалит ссылку на него и код в методе Receive будет выполняться бесконечно. Итак, как выглядит теперь моя Page:
На строке 8 я добавил наследование IRecepient<T> с указанием типа сообщения. Можно наследоваться на приём любого числа сообщений.
Значит, я обязан реализовать метод Receive, что и сделал на 27 строке и сразу же вывожу сообщение на экран
А подписку и отписку я реализовал через события Loaded и Unloaded: как только страница загрузилась, мы подписались, а когда она выгрузилась, мы отписались, множественные вызовы не произойдут.
Всё, давайте посмотрим результат:
Да, я тут над красотой не запаривался, но результат достигнут: команда и DockablePane ничего не знают о существовании друг друга, но мы смогли передать результат из одного класса в другой! Супер.
Минусы подхода с сообщениями
Я выделю тут 2 минуса, я не считаю их прям сильными минусами, я просто учитываю их при разработке:
1. Неожидаемое поведение при отладке другим разработчиком. Минус весьма относительный: новый разработчик может не понять, почему у нас действия в одном модуле влияют на другой, как так. Но контраргумент очень простой: он же видит "StrongReferenceMessenger.Default.Send", значит и быстро поймёт, в чём дело и где искать причины такого поведения
2. Повышение связности кода и риск возникновения сайд-эффектов.
Формально наши классы ничего не знают друг о друге и разделены. В реальности же они теперь немного зависят друг от друга. Да, эту зависимость легко разорвать, но всё же, теперь баг или изменение кода в одном модуле могут неожиданно повлиять на другой. И другая проблема — если мы отправили это сообщение из другого места с другой целью, то у нас триггер на его получение сработает дважды, а это не то, что мы хотим
Поэтому на каждое логическое действие, требующее обмена сообщениями, мы создаём новый тип сообщения, а код с их отправкой получением пишем внимательно и без причины и без тестирования не удаляем и не добавляем.
Заключение
Несмотря на указанные в конце минусы, я считаю Messaging хорошим инструментом, который приносит пользу в умелых руках, да и даже в неумелых что-то сломает с малой вероятностью. Используйте этот инструмент с умом и не добавляйте его там, где не надо — простой код всегда лучше сложного. До новых встреч!
Коммит с итоговым кодом на моём GitHib. Не забывайте подписываться на мой гитхаб и ставить звёздочки моим репозиториям, мне это очень приятно.
И конечно же, заглядывайте в мой телеграм-канал о Revit API, там тоже подписывайтесь. Пока!