Поведение при подписке на события

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

Для иллюстрации рассмотрим следующий пример. Пусть у нас есть некоторое приложение со списком опций и общей картинкой. Т.е. приложение выглядит примерно следующим образом:

sample

Логика работы приложения выглядит следующим образом. При выборе элемента из списка картинка предпросмотра обновляется на произвольную из выбранного альбома. Согласитесь, что все очень просто, так?

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

Код в реальности выглядит вот так. Т.е. это то, как вы его напишите с первого раза. Собственно я написал так же.

Не будем пока акцентировать внимание на разные аспекты окружения. Я думаю, что представленный код в целом понятен.

Итак, что вы ожидаете от такого кода? Посмотрите внимательно на названия методов, событий – решите для себя, что вы ожидаете.

Я думаю, что большинство из вас подумало, что выберется следующий элемент из списка и покажется картинка из выбранного элемента, так?

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

Это очень странно на первый взгляд, так как я подписываюсь на событие и ожидаю, что его получат все заинтересованные компоненты, и обработка не прервется на самом интересном месте. Для того, чтобы сработало передвижение на новый элемент из listBox, необходимо дописать IsHandled = false:

С другой стороны, если совсем внимательно и формально отнестись к названию события, то получаем что «команда получена, а дальше вы сами разбирайтесь как с ней поступать». Но тогда странно, что это событие.

На мой взгляд надо сделать два события:

  • ArrowNavigationCommandReceived – нет возможность использовать IsHandled для прерывания цепочки обработки.
  • ArrowNavigationCommandReceiving – есть возможность использовать IsHandled.

Ближайшая аналогия прослеживается с событиями Closed и Closing для окон (WinForm, WPF).

Что вы думаете по этому поводу? Комментарии как всегда приветствуются.

UPD: голосование пока что запилить не могу.

[poll id=»2″]

Hard’n’Heavy!

GC & Events

Недавно прочитал книгу Under the Hood of .Net Management от компании Red Gate и хочу всем ее посоветовать к прочтению, так как написана она очень доступно и хорошо, с картинками, примерами и рекомендациями. Вообще тема управления памятью в .Net достаточно интересна и познавательна, но в большинстве ресурсов описание ее идет с какими-то неимоверными сложностями или же с недостаточно наглядными примерами, на мой взгляд. Отчего не остается в голове цельной картины как же все работает в теории, так как авторы книги признаются, что нет точного описания управления памятью и того как сборщик мусора выполняет свою работу. Есть общие положения и структуры, но все остальное очень сложное и в 99,999% случаев не требуется знать всю начинку и как-то подсказывать сборщику мусора как работать. Т.е. в применении к сборщику мусора справедливо высказывание «помогать – только портить».

Итак, по мотивам книги хочу коротко пересказать как работает сборщик мусора, как авторы советуют писать метод Dispose() и некоторые мои соображения и эксперименты с событиями (которые event) в этом ключе.

Подробнее

Эволюция сервисных методов

Сложность по шкале Microsoft 200-300

Давно хотел изложить свои мысли по поводу эволюции сервисных методов, но до последнего момента правильная, на мой взгляд, идея имела плохую реализацию – слишком трудоемкую и развесистую. На данный момент я пришел к некоторому сокращению кода и готов поделиться мыслями по поводу эволюции сервисных методов. Сразу скажу, что речь пойдет о доменных сервисах по обработке данных, так как чаще всего именно они занимаются обработкой данных в течение продолжительного времени. Однако описанные подходы наверно можно применять и к другим классам с «долгоиграющими» методами.

В данном случае под эволюцией подразумевается то, как инфраструктурно выглядит метод, а не его функциональная эволюция (рефакторинг). Описывать, как всегда, я собираюсь свой личный опыт,  то, на чем я набил шишки.

Простейшие

Начиная со школы или университета, и в продолжение достаточно долгого времени мы пишем только однопоточные приложения. Все действия в них выполняются последовательно, синхронно. В этом нет ничего плохого в целом, многие методы и библиотеки работают синхронно и это хорошо. Не стоит городить сложность там, где она не нужна. Итак, простейший сервис может выглядеть следующим образом:

Не будем разводить дискуссий по поводу вырожденности примера, мол, большую часть времени есть аргументы и возвращаемое значение – суть от этого не поменяется. Сколько бы раз я не написал вызов этого метода, он будет выполнен последовательно, один за другим.

Со временем начинает расти недовольство тем, что интерфейс программы, если он есть, замирает в результате выполнения цепочки последовательных методов и в этот момент ни отменить операции, ни что-то сделать еще. С одной стороны есть «плюс», не надо делать уведомление о том, что операция закончилась, так как экран «отмерзнет», но лучше это делать более цивилизованными способами.

Зато, кстати, не надо было задумываться о том, что этот код может выполняться сразу в нескольких потоках, никаких проверок, что метод уже запущен и работает с данными. Второй раз с интерфейса все равно не запустишь его, если есть такая связь.

Подробнее

Иерархичные структуры данных и промежуточные расчеты

Традиционной, в плане изучения программирования в школах, университетах, курсах повышения квалификации и так далее – является архитектура, в которой мы явно управляем ходом выполнения программы. Это начинается с фактически процедурного турбо паскаля и идеологически продолжается в «рабочих» для бизнес-программирования языках (С# и Java). Каждое действие программы задается явно, т.е. если надо что-то пересчитать после добавления нового элемента в коллекцию, то так и пишут: сервис Х пересчитай мне коллекцию с помощью метода Z. Это – push модель, мы толкаем/принуждаем систему сделать определенные действия.

Более интересной является pull модель, когда объекты сами говорят о том, что с ними следует сделать. При той же ситуации, что описана выше, надо только добавить объект в коллекцию, а дальше уже коллекция сама может сказать сервису Х пересчитать себя с помощью метода Z. Система реагирует на события внутри себя.

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

Подробнее

Постоянные подписчики

При разработке сервисов для небольшой программы вы, скорее всего, не думаете о многопоточности, о том, что какой-либо метод сервиса будут запускать сразу несколько экземпляров класса и много еще о чем обычно не думаете. Так же как и я. В небольших программах, которые, скорее всего, будут исполняться в один поток, не надо развитой системы событий. Можно просто передать делегат, который будет вызван после окончания работы сервиса.

Может быть обстоятельства, а может быть что-то уложилось в голове, но описанный выше подход больше не работает в программах, которые я пишу. Слишком много действий одновременно может делать пользователь и начинка программы, чтобы оставаться в рамках модели «один сервис – один потребитель». Однако с новым подходом меняется сложность и ответственность кода, и на этом пути поджидают многие сложности и опасности.

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

Повторно подписались. Самая частая ошибка, которая вводит в ступор людей, это когда они обнаруживают:

  • кратные прибавки в вычислениях
  • двойные записи в базе данных
  • прочие казусы вычислений и отображений данных.

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

Забыли отписаться. Бывает так, что повторной подписки не происходит, но и отписаться от события забыли. В этом случае могут возникнуть утечки памяти, так как сервис держит класс и не дает сборщику мусора пометить объект на удаление.

К слову, не всегда бывает удобно отписываться и хочется, чтобы подписка была «слабой», как WeakReference. Тогда, если на объект нет внешних ссылок, кроме слабых подписок, то сборщик мусора пометит его на удаление, подписка освободится, память освободится и будет небольшое программистское счастье. Собственно, об этом и пойдет речь, как реализовать и использовать в своем приложении слабую подписку на события. Попутно я планирую раскрыть еще несколько рабочих моментов при использовании событий и сервисов.

Подробнее