Підводні камені WPF

Кожен, хто досить довгий час розробляв програми з використанням WPF, напевно, помічав, що цей фреймворк далеко не так простий у використанні, як може здатися на перший погляд. У цій статті я спробував зібрати деякі найбільш типові проблеми і способи їх вирішення.

  1. Засмічення пам'яті примірниками ResourceDictionary
  2. Витоку пам'яті
  3. Спадкування візуальних компонентів і стилі
  4. Помилки байндинга
  5. Стандартні засоби валідації
  6. Неправильне використання події PropertyChanged
  7. Надмірне використання Dispatcher
  8. Модальні діалоги
  9. Аналіз продуктивності відображення
  10. І ще трохи про INotifyPropertyChanged
  11. Замість післямови
Засмічення пам'яті примірниками ResourceDictionary
Найчастіше розробники явно включають необхідні словники ресурсів прямо в XAML розмітці користувача елементів управління от таким чином:

<UserControl x:Class="SomeProject.SomeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Styles/General.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>

На перший погляд, у такому підході немає ніякої проблеми — просто для елемента керування вказуємо мінімально необхідний набір стилів. Припустимо, в нашому додатку SomeControl існує у 10 примірниках на одному з вікон. Проблема полягає в тому, що при створенні кожного з цих примірників, зазначений словник буде заново вычитываться, оброблятися і зберігатися в окремій копією в пам'яті. Чим більше підключаються словники, чим більше примірників — тим більше потрібно часу на ініціалізацію містить їх подання і тим більше пам'яті витрачається впусту. Мені на практиці доводилося мати справу з додатком, в якому перевитрата пам'яті із-за зайвих ResourceDictionary був близько 200 мегабайт.

Мені відомо два варіанти вирішення цієї проблеми. Перший — підключати всі необхідні словники стилів тільки в App.xaml і більше ніде. Цілком може підійти для невеликих додатків, але для складних проектів може бути неприйнятним. Другий — замість стандартного ResourceDictionary використовувати його спадкоємця, який кешує словники таким чином, що кожен з них зберігається в пам'яті тільки в одному примірнику. На жаль, WPF з якоїсь причини не надає таку можливість «з коробки», але її легко реалізувати самостійно. Одне з найбільш повних рішень можна знайти в останньому відповіді тут   http://stackoverflow.com/questions/6857355/memory-leak-when-using-sharedresourcedictionary.

Витоку пам'яті
Витоку на події
Навіть у середовищі з автоматичним складанням сміття можна легко отримати витоку пам'яті. Найбільш часта причина витоків, і не тільки в WPF проектах — підписка на події без подальшого видалення обробника. Хоч це і не проблема самої технології, на ній варто зупинитися детальніше, оскільки в WPF проектах події використовуються часто і ймовірність появи помилки висока.

Наприклад, в програмі є список об'єктів, властивості яких можна змінювати в вікні редагування. Для реалізації цього вікна знадобилося встановлювати IsModified в true всередині його моделі представлення при зміні будь-якого властивості змінюваного об'єкта.

Припустимо, модель подання для редагування реалізована так:

public class EntityEditorViewModel
{
//...

public EntityEditorViewModel(EntityViewModel entity)
{
Entity = entity;
Entity.PropertyChanged += (s, e) => IsModified = true;
}
}

Тут конструктор встановлює сильну посилання між сутністю і моделлю подання редактора. Якщо створювати екземпляр EntityEditorViewModel при кожному показі вікна, то такі об'єкти будуть накопичуватися в пам'яті і віддаляться тільки в тому випадку, якщо посилається на них бізнес-сутність стане «сміттям».

Один з варіантів вирішення проблеми — передбачити видалення обробника. Наприклад, реалізувати IDisposable і метод Dispose() «відписуватися» від події. Але тут відразу варто сказати, що обробники, задані лямбда-виразами як у прикладі, не можуть бути видалені простим способом, тобто ось таке не спрацює:

//Цей код не буде працювати коректно!
entity.PropertyChanged -= (s, e) => IsModified = true;

Для правильного розв'язання задачі потрібно оголосити окремий метод, помістити в нього установку IsModified і використовувати в якості обробника, як завжди робилося до появи лямбда-виразів в C#.

Але підхід з явним видаленням не гарантує відсутність витоків пам'яті — можна банально забути покликати Dispose(). Крім цього, може бути дуже проблематично визначити той момент, коли треба його викликати. В якості альтернативи можна розглянути більш громіздкий, але дієвий підхід — Weak Events. Загальна ідея їх реалізації в тому, що між джерелом події і передплатником встановлюється «слабка» посилання, і передплатник може бути автоматично видалений, коли на нього більше не стане «сильних» посилань.

Пояснення реалізації патерну Weak Events виходить за рамки цього статті, тому просто покажу посилання, де ця тема розглянута дуже докладно: http://www.проект коду (codeproject).com/Articles/29922/Weak-Events-in-C.

Витоку при байндинге
Крім потенційної проблеми, описаної вище, в WPF є як мінімум два типи витоків, які специфічні саме для цієї технології.

Припустимо, у нас є простий об'єкт:

public class SomeModelEntity
{
public string Name { get; set; }
}

І ми прив'язуємося до цієї властивості з будь-якого елемента керування:

<TextBlock Text="{Binding Entity.Name, Mode=TwoWay}" />

Якщо властивість, до якого йде прив'язка, не є DependencyProperty, або об'єкт, що містить його, не реалізує INotifyPropertyChanged — механізм байндинга використовує подія ValueChanged класу System.ComponentModel.PropertyDescriptor для відстеження змін. Проблема тут у тому, що фреймворк тримає у себе посилання на екземпляр PropertyDescriptor, який у свою чергу посилається на вихідний об'єкт, і неясно, коли цей примірник можна буде видалити. Слід зазначити, що у випадку з OneTime байндингом проблема не актуальна, так як не потрібно відслідковувати зміни.

Інформація про цієї проблеми є і в Microsoft Knowledge Base: https://support.microsoft.com/en-us/kb/938416, але в ній зазначено одна додаткова умова виникнення витоку. Якщо застосувати його до попереднього прикладу, то отримаємо, що примірник SomeModelEntity повинен прямо чи побічно посилатися на TextBox, щоб стався витік. З одного боку, така умова досить рідко виконується на практиці, але в реальності завжди краще дотримуватися більш «чистого» підходу — або явно вказувати OneTime режим байндинга, якщо не потрібно стежити за змінами, або реалізовувати INotifyPropertyChanged на об'єкті-джерелі, або робити властивість DependencyProperty (має сенс для властивостей візуальних компонентів).

Інша можлива проблема при установці байндингов — прив'язка до колекцій, які не реалізують інтерфейс INotifyCollectionChanged. Механізм виникнення витоків в цьому випадку дуже схожий на попередній. Спосіб боротьби очевидний — потрібно або явно вказувати OneTime режим прив'язки, або використовувати колекції, що реалізують INotifyCollectionChanged — наприклад, ObservableCollection.

Спадкування візуальних компонентів і стилі
Іноді виникає потреба у спадкуванні стандартних елементів управління для розширення їх функціональності, зміни поведінки. На перший погляд, це елементарно:

public class CustomComboBox :ComboBox
{
//...
}

Але якщо в програмі використовуються стилі елементів, відмінні від стилів за замовчуванням, проблема при використанні такого спадкоємця буде помітна відразу. Наступний фрагмент скріншота показує різницю у відображенні базового елемента управління і похідного при включеній темі PresentationFramework.Aero.



Найпростіший спосіб це виправити — в XAML файлі після включення ресурсів теми визначити стиль для похідного елемента, як успадкованого від базового. Це легко здійснюється за допомогою атрибута BasedOn:

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/PresentationFramework.Aero;component/themes/Aero.NormalColor.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:my Type:CustomComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
</Style>
</ResourceDictionary>
</Application.Resources>


Але виходить, що при використанні похідного елемента керування потрібно завжди пам'ятати і про додавання стилю в ресурси. Або ж зробити файл з цим похідним стилем і підключати його кожен раз, коли потрібно використовувати новий елемент.

Є один спосіб обійтися без змін в XAML — в статичному режимі конструктора похідного елемента явно встановлювати йому стиль, взятий з базового:

static CustomComboBox()
{
StyleProperty.OverrideMetadata(typeof(CustomComboBox),
new FrameworkPropertyMetadata(new Style(typeof(CustomComboBox), 
(Style)Application.Current.TryFindResource(typeof(ComboBox)))));
}

Але проблема такого варіанта в тому, що копіювання стилю буде відпрацьовувати тільки під час виконання програми та відображення у дизайнері цей код не вплине. Тому, якщо потрібні «правильні» стилі і в дизайнері, краще скористатися попереднім варіантом.

Помилки байндинга
Декларативне зв'язування елементів управління з полями моделі, звичайно ж, має свої переваги, але за його цілісністю не так вже просто стежити. Якщо з якоїсь причини властивість, вказане в байндинге, не знайдено — помилка буде написана в лог налагодження… І все. За замовчуванням користувач не побачить ніяких повідомлень, при запуску без налагодження ні в яких логах цих помилок не буде.

Щоб зробити такі помилки більш помітними для розробника, можна написати спеціальний Trace Listener, який буде виводити їх у вигляді повідомлень:

public class BindingErrorTraceListener : TraceListener
{
private readonly StringBuilder _messageBuilder = new StringBuilder();

public override void Write(string message)
{
_messageBuilder.Append(message);
}

public override void WriteLine(string message)
{
Write(message);

MessageBox.Show(_messageBuilder.ToString(), "Binding error", MessageBoxButton.ОК, MessageBoxImage.Warning);
_messageBuilder.Clear();
}
}

І потім активувати його при старті програми:

PresentationTraceSources.DataBindingSource.Listeners.Add(new BindingErrorTraceListener());
PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Error;

Після цих змін кожна помилка байндинга буде виводитися у вигляді діалогового повідомлення, але тільки при запуску з налагодженням, тому має сенс скористатися умовної компіляцією, щоб у релізних версіях обробник не реєструвався.

Стандартні засоби валідації
В WPF існує кілька способів валідації даних.

ValidationRule — наслідуючи цей клас можна створювати спеціалізовані правила валідації, які потім прив'язуються до полів в розмітки XAML. З «умовних» плюсів — не потрібні зміни класів моделі для виконання валідації, хоча в деяких випадках це може бути не самим оптимальним варіантом. Але при цьому є значний недолік — ValidationRule не успадковує DependencyObject, відповідно до спадкоємців немає можливості створювати властивості, на які згодом можна буде байндиться. Це означає, що немає простого очевидного способу проводити валідацію властивостей у зв'язці один з одним — наприклад, якщо значення одного не може бути більше значення іншого. Валидационное правило, реалізоване таким способом, може мати справу тільки з поточним значенням поля і фіксованими значеннями, які були вказані при створенні екземпляра цього правила.

IDataErrorInfo, INotifyDataErrorInfo — реалізуючи ці інтерфейси в класах моделей представлення можна легко здійснювати валідацію як окремих властивостей, так і декількох властивостей у зв'язці один з одним. Зазвичай для зменшення кількості коду один з цих інтерфейсів реалізують в базовому класі моделей і надають кошти лаконічного опису правил у спадкоємців. Наприклад, за реєстрацію правил в статичному режимі конструктора для кожного з типів:

static SomeModelEntity()
{
RegisterValidator(me => me.Name, me => !string.IsNullOrWhiteSpace(me.Name), 
Resources.RequiredFieldMessage);
}

Або через атрибути:

[Required]
public string Name 
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged();
}
}

Гарний опис другого варіанту можна знайти за адресою http://www.проект коду (codeproject).com/Articles/97564/Attributes-based-Validation-in-a-WPF-MVVM-Applicat.

Але і підхід з використанням DataErrorInfo інтерфейсів не покриває всі валидационные завдання — у випадках, коли для перевірки правила потрібно мати доступ до об'єктів за межами валидируемой суті, починають виникати проблеми. Наприклад, перевірка на унікальність вимагає доступу до повної колекції об'єктів, а це означає, що кожен елемент такої колекції повинен мати посилання на неї, що сильно ускладнює роботу з об'єктом.

На жаль, стандартних засобів, що дозволяють легко обійти цю проблему, в WPF немає, і доводиться писати щось своє. В найпростішому випадку, якщо перед збереженням запису повинна бути перевірена її унікальність, це можна зробити явно в коді перед викликом збереження і показати повідомлення в разі помилки.

Такий підхід насправді теж можна узагальнити. Скористаємося згаданої вище ідеєю з реєстрацією валідаторів в статичному режимі конструктора. Ось приклад базового класу:

public class ValidatableEntity<TEntity> : IDataErrorInfo
{
//Для реєстрації "звичайних" валідаторів
protected static void RegisterValidator<TProperty>(
Expression<Func<TProperty>> property,
Func<TEntity, bool> validate,
string message)
{
//...
}

//Для валідаторів, яким потрібен доступ до об'єктів за межами сутності - наприклад, для перевірки на унікальність
protected static void RegisterValidatorWithState<TProperty>(
Expression<Func<TProperty>> property, 
Func<TEntity, object, bool> validate, 
string message)
{
//...
}

public bool Validate(object state, out IEnumerable < string> errors)
{
//Викликає всі зареєстровані валідатори та агрегує всі знайдені помилки. У функції, зареєстровані через RegisterValidatorWithState, передає об'єкт state в якості другого параметра.
}

//Реалізує IDataErrorInfo, використовуючи тільки функції, зареєстровані через RegisterValidator
}

А також приклад використання:

public class SomeModelEntity : ValidatableEntity<SomeModelEntity>
{
public string Name { get; set; }

static SomeModelEntity()
{
RegisterValidator(me => me.Name, me => !string.IsNullOrWhiteSpace(me.Name),
Resources.RequiredFieldMessage);

RegisterValidatorWithState(me => me.Name, 
(me all) => ((IEnumerable<SomeEntity>)all).Any(e => e.Name == me.Name),
Resources.UniqueNameMessage);
}
}

Таким чином, всі валидационные правила знаходяться всередині самої сутності. Ті з них, які не вимагають «зовнішніх» об'єктів, що використовуються в реалізації IDataErrorInfo з базового класу. Для перевірки інших досить покликати функцію Validate в потрібному місці і використовувати результат для прийняття рішень про подальші дії.

Неправильне використання події PropertyChanged
Мені досить часто доводилося зустрічати код подібного виду в WPF проектах:

private void someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Quantity")
{
//Якась логіка, яка може, у свою чергу, змінювати інші властивості
}
}

Причому у багатьох випадках це був обробник власних подій, тобто «слухав» зміни властивостей того ж класу, де і був оголошений.

У такому підході є кілька серйозних недоліків, які в підсумку призводять до дуже важкого в плані підтримки і розширення коду. Деякі з них очевидні: наприклад, при перейменуванні властивостей можна забути змінити константи в умовах, але це дрібний і легко вирішується недолік. Набагато серйозніша проблема в тому, що при такому підході буває практично неможливо відстежити всі сценарії, при яких виконується той чи інший шматок логіки.

Можна сформулювати наступний критерій для самоперевірки, чи правильно використовується обробник події PropertyChanged: якщо алгоритм всередині обробника не залежить від конкретних назв властивостей, то все в порядку. В іншому випадку потрібно шукати більш вдале рішення. Прикладом правильного застосування може бути, наприклад, установка властивості IsModified в true при зміні якого-небудь властивості моделі подання.

Надмірне використання Dispatcher
Неодноразово зустрічав в WPF проектах примусове виконання операцій на UI потоці навіть у тих випадках, коли це не потрібно. Для того, щоб описати масштаб проблеми, наведу кілька цифр, отриманих з допомогою простих тестів на ноутбуці з процесором Core i7-3630QM 2.4 GHz:

  • Час, витрачений Dispatcher.Invoke понад «корисною» навантаження при виклику з того ж потоку, до якого належить Dispatcher — 0.2 мкс на один виклик.
  • Той же показник, але при виклику з іншого потоку — 26 мкс на виклик.
Перша цифра не здається страшною, але і викликати що-то через Dispatcher, коли відомо, що код і так буде виконуватися на UI потоці — теж неправильно. А ось друга цифра вже виглядає помітною. Слід врахувати, що в реальних складних додатках, особливо при диспетчеризації з декількох паралельних потоків, цей час може бути значно більше. А на більш слабких пристроях   ще більше.

Щоб зменшити шкоду для продуктивності, досить дотримуватися простих правил:

  • Диспетчеризувати тільки те, що дійсно не можна виконати на фоновому потоці. Наприклад, є шматок коду, який щось читає з WEB-сервісу, потім робить розрахунок з якогось алгоритму, потім встановлює пару властивостей моделі подання. У цьому випадку тільки установка властивостей повинна диспетчеризироваться (т. к. в свою чергу викликає обробники PropertyChanged, серед яких є код, який працює з UI).
  • Уникати циклів, всередині яких є звернення до Dispatcher. Наприклад, потрібно прочитати список з сервера, і за даними кожного елемента зробити оновлення UI. В цьому випадку краще спочатку прорахувати на фоновому потоці все, що потрібно буде оновлювати на UI, і тільки потім одним викликом Dispatcher.Invoke зробити оновлення. Виклик Invoke після обробки кожного елемента списку буде вкрай неоптимальним рішенням.


Модальні діалоги
Використання стандартних модальних повідомлень (MessageBox) в WPF проектах не вітається, так як кастомизировать їх зовнішній вигляд у відповідності з візуальними стилями додатки просто неможливо. Замість стандартних повідомлень доводиться писати свої реалізації, які можна умовно розділити на два типи:

  • Окреме модальне вікно (Window.ShowDialog), стилізоване потрібним чином.
  • «Емуляція» модального вікна через додавання панелі візуальне дерево основного вікна, що знаходиться «над» усім іншим вмістом, тим самим перекриваючи його.
У кожного підходу є свої плюси і мінуси. Перший варіант простий в реалізації, але не дозволяє досягти таких ефектів, як «затемнення» всього вмісту вікна, над яким показується модальний діалог. У деяких програми може виникнути потреба в діалозі нестандартної форми, що з звичайним вікном зробити не зовсім просто.

Другий варіант звичайно викликає безліч проблем в реалізації, які викликані тим, що показ такого вікна не може бути синхронним. Тобто, не можна буде написати як з звичними повідомленнями:

if (MessageBox.Show(Resources.ResetSettingsQuestion, Resources.ResetSettings, 
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{

і чекати на основному потоці більше нічого не може статися, поки користувач не відповість на питання.

Розглянемо одну з найбільш простих реалізацій «эмулированного» діалогу.

В першу чергу оголосимо інтерфейс менеджера діалогового вікна, через який модель представлення буде показувати діалоги. Для початку не будемо враховувати можливість отримувати «відповідь» від вікна — просто покажемо діалог з кнопкою «Закрити».

public interface IModalDialogHelper
{
public string Text { get; }

ICommand CloseCommand { get; }

void Show(string text);

void Close();
}

Далі реалізуємо елемент управління, який буде «прив'язуватися» до менеджера і показувати вікно поверх інших елементів, коли це необхідно:

<UserControl x:Class="TestDialog.ModalDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
mc:Ignorable="d" 
d:DesignHeight="300" d:DesignWidth="300" 
Panel.ZIndex="1000">
<UserControl.Style>
<Style TargetType="{x:Type UserControl}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding DialogHelper.IsVisible}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Style>
<Grid>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="DarkGray"
Opacity=".7" />
<Grid HorizontalAlignment="Stretch" Height="200" Background="AliceBlue">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Авто" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding DialogHelper.Text}" />
<Button Grid.Row="1" Content="Close" Command="{Binding DialogHelper.CloseCommand}" HorizontalAlignment="Right" />
</Grid>
</Grid>
</UserControl>

Знову ж таки, для спрощення, цей контрол розрахований на те, що в моделі подання є примірник реалізації IModalDialogHelper властивості DialogHelper. В більш універсальному рішенні повинна бути можливість підставляти будь-яку властивість.

Я не буду тут наводити приклад найпростішої реалізації IModalDialogHelper, так як вона очевидна: методи Show() і Close() встановлюють відповідним чином IsVisible, команда CloseCommand просто викликає метод Close(). Show() встановлює властивість Text.

Начебто все просто: викликаємо метод Show() з потрібним текстом повідомлення, він робить видимим панель з повідомленням і кнопкою, наступне натискання на кнопку Close встановлює IsVisible у вихідне значення і «діалог» зникне з екрана. Але тут вже є перша проблема — послідовний показ кількох повідомлень призводить до того, що користувач бачить тільки останнє, так як метод Show() не очікує закриття попереднього діалогу.

Для рішення цієї проблеми трохи змінимо прототип методу Show:

Task Show(string text);

Можливість очікувати завершення цього методу через await дає відразу кілька переваг:

  • Показ кількох повідомлень з одного і того ж потоку буде коректно працювати, на відміну від попереднього прикладу.
  • Можна реалізувати метод так, що навіть виклик повідомлення з різних потоків буде працювати коректно і чекати закриття вже показаного діалогу.
  • Можна повертати модальний результат як у «старому» MessageBox.Show().
Тут я наведу один з варіантів реалізації інтерфейсу IModalDialogHelper з асинхронним Show, яка відповідає першим двом пунктам зі списку вище (додати модальний результат в цю реалізацію не складе праці):

class ModalDialogHelper : INotifyPropertyChanged,
IModalDialogHelper
{
private readonly Queue<Tuple<Task, AutoResetEvent>> _waits =
new Queue<Tuple<Task, AutoResetEvent>>();
private readonly object syncObject = new object();
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;

//...

public async Task Show(string text)
{
List<Tuple<Task, AutoResetEvent>> previousWaits;
Task waitTask;
AutoResetEvent waitEvent;

lock (syncObject)
{
//Запам'ятовуємо список завдань, які треба чекати
previousWaits = _waits.ToList();

//Створюємо завдання і подія для даного конкретного повідомлення
waitEvent = new AutoResetEvent(false);
waitTask = Task.Run(() => waitEvent.WaitOne());
_waits.Enqueue(new Tuple<Task, AutoResetEvent>(waitTask, waitEvent));
}

//Чекаємо завершення завдань, які вже є в черзі
foreach (var wait in previousWaits)
{
await wait.Item1;
}

//Цей блок повинен бути виконаний на основному потоці
_dispatcher.Invoke(() =>
{
Text = text;
IsVisible = true;
});

await waitTask;

waitEvent.Dispose();
}

public void Close()
{
IsVisible = false;

Tuple<Task, AutoResetEvent> wait;

lock (syncObject)
{
//Прибрати поточну завдання з черги
wait = _waits.Dequeue();
}

wait.Item2.Set();
}

//... 
}

Основна ідея цього рішення полягає в тому, що для кожного виклику Show створюється екземпляр AutoResetEvent і завдання, яка просто чекає установки цієї події. Show до показу свого повідомлення чекає всі завдання, які вже є в черзі, після показу — чекає на свою власну, а Close «активує» AutoResetEvent, тим самим завершуючи завдання, відповідну поточного діалогу.

І слід сказати ще пару слів про використання «нових» діалогів в обробниках подій типу CancelEventHandler. Підтвердження дій у таких подіях теж потрібно буде реалізовувати трохи не так, як раніше.

//Цей код не буде працювати коректно!
private async void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
if(await dialogHelper.Show("Do you really want to close the window", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
e.Cancel = false;
}
}

Проблема в тому, що e.Cancel завжди буде true для коду, що викликав Window_Closing, так як await не зупиняє виконання потоку, а створює можливість «повернутися» в потрібне місце в методі після завершення асинхронної завдання. Для викликав коду, Windows_Closing завершиться одразу після встановлення e.Cancel в true.

Правильне рішення полягає в тому, що тіло умови має оперувати вже не e.Cancel, а явно викликати «скасоване» дія таким чином, щоб воно гарантовано виповнилося без додаткових запитів, минаючи повторний виклик цього обробника. У разі закриття головного вікна програми, наприклад, це може бути явний виклик завершення програми.

Аналіз продуктивності відображення
Багато розробники знають, що таке «профайлер» і знають, які є засоби для аналізу продуктивності програми та аналізу споживання пам'яті. Але в WPF додатках частину навантаження на процесор виходить, наприклад, з механізму обробки розмітки XAML – парсинг, розмітка, малювання. «Стандартними» профайлерами непросто визначити, на яку саме активність, пов'язану з XAML, витрачаються ресурси.

Не буду детально зупинятися на можливостях існуючих інструментальних засобів, просто перерахую посилання на інформацію про них. Розібратися з тим, як ними користуватися, не складе праці будь-якому розробнику.



І ще трохи про INotifyPropertyChanged
Одна з найпопулярніших тем спорів в рамках технології WPF — як найбільш раціонально реалізовувати INotifyPropertyChanged. Самий лаконічний варіант — використовувати АОП, як я вже описував в одному з прикладів статті про Aspect Injector. Але не всім цей підхід подобається, і в якості альтернативи можна використовувати фрагменти. Але тут виникає питання про найбільш оптимальному вмісті фрагменту. Спершу наведу приклади найбільш вдалих варіантів.

private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}

В даному випадку ім'я властивості зазначено константою, і не важливо   буде воно в іменованої константи або, як у прикладі, «захардкоджено» прямо у виклику методу оповіщення — проблема залишається тією ж: перейменування самого властивості існує ймовірність залишити старе значення константи. Цю проблему вирішують багато наступним зміною методу NotifyPropertyChanged:

public void NotifyPropertyChanged<T>(Expression<Func<T>> property)
{
var handler = PropertyChanged;
if(handler != null)
{
string propertyName = ((MemberExpression)property.Body).Member.Name;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

У цьому випадку замість назви можна вказати лямбда-вираз, який повертає потрібне властивість:

NotifyPropertyChanged(() => Name);

На жаль, цей варіант теж має недоліки — виклик такого методу завжди пов'язаний з Reflection, що за підсумком у сотні разів повільніше виклику попереднього варіанту NotifyPropertyChanged. У разі додатків для мобільних пристроїв це може бути критичним.

В .NET 4.5 став доступним спеціальний атрибут CallerMemberNameAttribute, завдяки яким можна вирішити першу з наведених вище проблем:

public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
//...
}

public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged();
}
}

Якщо параметр, позначений цим атрибутом, не вказаний явно, компілятор додасть в нього ім'я члена класу, викликає метод. Таким чином, виклик NotifyPropertyChanged() з прикладу вище рівнозначний NotifyPropertyChanged(«Name»). Але що робити, якщо потрібно повідомити про зміну якої-небудь властивості «зовні», не з його сетера?

Наприклад, у нас є «калькулируемое» властивість:

public int TotalPrice
{
get { return items.Sum(i => i.Price); }
}

Додавання, видалення або зміну елементів колекції items нам потрібно повідомляти про зміну TotalPrice, щоб інтерфейс користувача завжди відображав його актуальне значення. Враховуючи недоліки перших двох рішень, наведених вище, можна зробити наступний хід — все-таки використовувати Reflection для отримання імені властивості з лямбда-виразу, але зберігати його статичної змінної. Таким чином для кожного окремо взятого властивості «важка» операція буде виконуватися тільки один раз.

public class ResultsViewModel : INotifyPropertyChanged
{
public static readonly string TotalPricePropertyName = ExpressionUtils.GetPropertyName<ResultsViewModel>(m => m.TotalPrice);

//...
NotifyPropertyChanged(TotalPricePropertyName);
//...
}

public static class ExpressionUtils
{
public static string GetPropertyName<TEntity>(Expression<Func<TEntity, object>> property)
{
var convertExpression = property.Body as UnaryExpression;
if(convertExpression != null)
{
return ((MemberExpression)convertExpression.Operand).Member.Name;
}

return ((MemberExpression)property.Body).Member.Name;
}
}

Саму статичну функцію GetPropertyName можна покласти і в базовий клас для всіх «notifiable» сутностей — це не принципово. Перевірка на UnaryExpression потрібна, щоб функція нормально обробляла властивості значущих типів, т. к. компілятор додає операцію боксингу, щоб привести зазначене властивість до object.

В якості підсумку можна сказати, що якщо використання АОП для INotifyPropertyChanged з якихось причин не влаштовує, то можна скористатися сниппета наступного змісту:

  • Якщо проект ще не перейшов на використання .NET 4.5 — для кожного властивості додавати статичне поле, яке при ініціалізації буде заповнюватися ім'ям властивості через функцію GetPropertyName, як показано вище. A сетер, в свою чергу, буде передавати в NotifyPropertyChanged значення цього поля.
  • Якщо .NET 4.5 вже використовується — то для більшості властивостей буде достатньо рішення з CallerMemberNameAttribute. А для тих випадків, коли цього рішення недостатньо — підійде варіант зі статичним полем імені властивості.


Замість післямови
WPF — непогана технологія, яку Microsoft як і раніше позиціонує як основний фреймворк для розробки «настільних» додатків. На жаль, при написанні програм складніше «калькулятора» виявляється ряд проблем, які не помітні з першого погляду, але всі вони вирішуються. Згідно з недавніми заявами Microsoft, вони інвестують в розвиток технології, і в новій версії вже є багато поліпшень. В першу чергу вони відносяться до інструментальним засобам і продуктивності. Хочеться сподіватися, що в майбутньому нові можливості будуть додані не тільки в інструментальні засоби, але і сам фреймворк, полегшуючи роботу програміста і позбавляючи від «хаків» і «велосипедів», які доводиться робити зараз.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.