📜 ⬆️ ⬇️

WPF pitfalls

Anyone who has been developing applications using WPF for quite a long time probably noticed that this framework is not as easy to use as it might seem at first glance. In this article I tried to collect some of the most typical problems and ways to solve them.

  1. ResourceDictionary clogged memory
  2. Memory leaks
  3. Inheritance of visual components and styles
  4. Buying errors
  5. Standard Validation Tools
  6. Incorrect use of the PropertyChanged event
  7. Overuse Dispatcher
  8. Modal dialogs
  9. Mapping performance analysis
  10. And a little more about INotifyPropertyChanged
  11. Instead of an afterword

ResourceDictionary clogged memory


Often, developers explicitly include the necessary resource dictionaries right in the XAML markup of user controls like this:

<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> 

At first glance, in this approach there is no problem - just for the control, we specify the minimum required set of styles. Suppose in our application SomeControl exists in 10 instances on one of the windows. The problem is that when creating each of these instances, the specified dictionary will be re-read, processed and stored in a separate copy in memory. The more dictionaries are connected, the more copies - the more time it takes to initialize the view containing them and the more memory is wasted. I had to deal in practice with an application in which the memory overrun due to the extra ResourceDictionary was about 200 megabytes.

I know two options for solving this problem. The first is to connect all the necessary style dictionaries only in App.xaml and nowhere else. It may well be suitable for small applications, but for complex projects may be unacceptable. The second - instead of the standard ResourceDictionary use his successor, which caches dictionaries in such a way that each of them is stored in memory only in one instance. Unfortunately, WPF for some reason does not provide such an opportunity out of the box, but it is easy to implement on your own. One of the most complete solutions can be found in the last answer here - http://stackoverflow.com/questions/6857355/memory-leak-when-using-sharedresourcedictionary .
')

Memory leaks


Event leaks


Even in an environment with automatic garbage collection, you can easily get memory leaks. The most common cause of leaks, and not only in WPF projects, is subscription to events without subsequent removal of the handler. Although this is not a problem of the technology itself, it is worthwhile to dwell on it, since WPF projects often use events and the probability of an error is high.

For example, the application has a list of objects whose properties can be changed in the editing window. To implement this window, it was necessary to set IsModified to true inside its view model when changing any property of the object being edited.

Suppose the view model for editing is implemented like this:

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

Here the constructor establishes a “strong” link between the business entity and the editor's view model. If you create an instance of EntityEditorViewModel each time the window is displayed, such objects will be accumulated in memory and deleted only if the business entity that references them becomes “garbage”.

One solution to the problem is to consider removing the handler. For example, implement IDisposable and in the Dispose () method "unsubscribe" from the event. But here it is right to say that the handlers specified by lambda expressions as in the example cannot be deleted in a simple way, i.e. Here it does not work:

 //     ! entity.PropertyChanged -= (s, e) => IsModified = true; 

To solve the problem correctly, you need to declare a separate method, place the IsModified installation in it and use it as a handler, as always was done before the appearance of lambda expressions in C #.

But the approach with explicit deletion does not guarantee the absence of memory leaks - you can simply forget to call Dispose (). In addition, it can be very problematic to determine the moment when you need to call it. Alternatively, you can consider a more cumbersome, but effective approach - Weak Events. The general idea of ​​their implementation is that a “weak” link is established between the source of the event and the subscriber, and the subscriber can be automatically deleted when there are no more “strong” links to it.

An explanation of the implementation of the Weak Events pattern is beyond the scope of this article, so I’ll just point out a link where this topic is covered in great detail: http://www.codeproject.com/Articles/29922/Weak-Events-in-C .

Binding Leaks


In addition to the potential problem described above, WPF has at least two types of leaks that are specific to this technology.

Suppose we have a simple object:

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

And we bind to this property from any control:

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

If the property to which the binding goes is not a DependencyProperty, or the object containing it does not implement INotifyPropertyChanged - the binding mechanism uses the ValueChanged event of the System.ComponentModel.PropertyDescriptor class to track changes. The problem here is that the framework holds a link to the PropertyDescriptor instance, which in turn refers to the original object, and it is unclear when this instance can be deleted. It should be noted that in the case of OneTime Bayding, the problem is not relevant, since it is not necessary to track changes.

Information about this problem is also in the Microsoft Knowledge Base: https://support.microsoft.com/en-us/kb/938416 , but it indicates one additional condition for the occurrence of a leak. If we apply it to the previous example, we’ll get that the SomeModelEntity instance must directly or indirectly refer to the TextBox for a leak to occur. On the one hand, such a condition is rarely fulfilled in practice, but in reality it is better to always adhere to a more “cleaner” approach — either explicitly specify the OneDime buyding mode if you do not need to follow the changes, or implement INotifyPropertyChanged on the source object, or make the DependencyProperty property (makes sense for the properties of visual components).

Another possible problem when installing banding is binding to collections that do not implement the INotifyCollectionChanged interface. The mechanism of occurrence of leaks in this case is very similar to the previous one. The way to fight is obvious - you must either explicitly specify the OneTime binding mode, or use collections that implement INotifyCollectionChanged — for example, ObservableCollection.

Inheritance of visual components and styles


Sometimes it is necessary to inherit standard controls to extend their functionality, change behavior. At first glance, this is elementary:

 public class CustomComboBox : ComboBox { //… } 

But if the application uses styles of elements other than the default styles, the problem with using such an inheritor will be immediately noticeable. The following screenshot shows the difference in the display of the base control and the derivative with the PresentationFramework.Aero theme enabled.



The easiest way to fix this is in the XAML file after including the theme resources to determine the style for the derived element, as inherited from the base element. This is easily accomplished using the BasedOn attribute:

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


But it turns out that when using a derived control, you should always remember to add a style to the resources. Or make a file with this derived style and connect it every time you need to use a new element.

There is one way to do without changes in XAML - in the constructor of the derived element it is obvious to set the style taken from the base one:

 public CustomComboBox() { SetResourceReference(StyleProperty, typeof(ComboBox)); } 

Thus, if you do not need to add any changes to the base style, this method will be the most optimal. Otherwise, it is better to use the previous option.

Buying errors


The declarative binding of controls to the fields of the model, of course, has its advantages, but its integrity is not so easy to follow. If for some reason the property specified in the binding is not found - the error will be written to the debug log ... And that's it. By default, the user will not see any messages, when started without debugging, no logs of these errors will appear.

To make such errors more noticeable to the developer, you can write a special Trace Listener, which will display them as messages:

 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.OK, MessageBoxImage.Warning); _messageBuilder.Clear(); } } 

And then activate it when the application starts:

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

After these changes, each binding error will be displayed as a dialog message, but only when started with debugging , so it makes sense to use conditional compilation so that the release agent is not registered in the release versions.

Standard Validation Tools


There are several ways to validate data in WPF.

ValidationRule — By inheriting this class, you can create specialized validation rules that are then bound to fields in the XAML markup. Of the “conditional” advantages, no change in the model classes is required to perform validation, although in some cases this may not be the most optimal option. But at the same time there is a significant drawback - the ValidationRule does not inherit a DependencyObject, respectively, in the heirs there is no possibility to create properties for which later it will be possible to link. This means that there is no simple obvious way to validate properties in conjunction with each other — for example, if the value of one cannot be greater than the value of the other. A validation rule implemented in this way can deal only with the current field value and fixed property values ​​that were specified when creating an instance of this rule.

IDataErrorInfo, INotifyDataErrorInfo - by implementing these interfaces in the view model classes, you can easily validate both individual properties and several properties in conjunction with each other. Usually, to reduce the amount of code, one of these interfaces is implemented in the base class of models and provides the means of a concise description of the rules in the heirs. For example, through the registration of rules in a static constructor for each of the types:

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

Or through attributes:

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

A good description of the second option can be found at http://www.codeproject.com/Articles/97564/Attributes-based-Validation-in-a-WPF-MVVM-Applicat .

But the approach using the DataErrorInfo interfaces does not cover all validation tasks - in cases where checking a rule requires access to objects outside the validated entity, problems begin to arise. For example, checking for uniqueness requires access to a complete collection of objects, which means that each element of such a collection must have a link to it, which greatly complicates the work with the object.

Unfortunately, there are no standard tools to easily get around this problem in WPF, and you have to write something of your own. In the simplest case, if its uniqueness should be verified before saving the record, this can be done explicitly in the code before the save is called and the message is shown in case of an error.

This approach can actually be generalized too. We use the idea mentioned above with registering validators in a static constructor. Here is an example of a base class:

 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 } 

As well as an example of use:

 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); } } 

Thus, all validation rules are within the entity itself. Those that do not require "external" objects are used in the implementation of IDataErrorInfo from the base class. To check the rest, it’s enough to call the Validate function in the right place and use the result to make decisions about further actions.

Incorrect use of the PropertyChanged event


I quite often had to meet code of this type in WPF projects:

 private void someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Quantity") { //- ,  ,   ,    } } 

And in many cases it was a handler of its own events, i.e. "Listened" to changes in properties of the same class, where it was declared.

In this approach, there are several serious flaws, which ultimately lead to very difficult in terms of support and expansion of the code. Some of them are obvious: for example, when renaming properties, you can forget to change the constants in the conditions, but this is a small and easily solved drawback. A much more serious problem is that with this approach it is almost impossible to track all the scenarios in which this or that piece of logic is executed.

We can formulate the following criterion for self-checking whether the PropertyChanged event handler is used correctly: if the algorithm inside the handler does not depend on the specific property names, then everything is fine. Otherwise, you need to look for a better solution. An example of the correct application can be, for example, setting the IsModified property to true when changing any property of the view model.

Overuse Dispatcher


Repeatedly encountered in WPF projects the enforcement of operations on the UI stream, even in cases where it is not necessary. In order to describe the scale of the problem, I’ll give a couple of numbers obtained using simple tests on a laptop with a Core i7-3630QM 2.4GHz processor:


The first digit does not look terrible, but calling something through the Dispatcher, when it is known that the code will be executed on the UI thread as well, is also wrong. But the second figure already looks noticeable. It should be noted that in real complex applications, especially when dispatching from several parallel threads, this time can be much longer. And on weaker devices - even more.

To reduce harm to performance, it’s enough to follow simple rules:



Modal dialogs


The use of standard modal messages (MessageBox) in WPF projects is not welcome, since it is simply impossible to customize their appearance in accordance with the visual styles of the application. Instead of standard messages, you have to write your own implementations, which can be divided into two types:


Each approach has its pros and cons. The first option is simple to implement, but does not allow to achieve such effects as “darkening” of the entire contents of the window, above which a modal dialog is displayed. In some applications, there may be a need for a dialogue of a non-standard form, which is not very easy to do with a regular window.

The second option usually causes many problems in the implementation, which are caused by the fact that the display of such a window cannot be synchronous. That is, it will not be possible to write as with familiar messages:

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

and expect that nothing can happen on the main thread until the user answers the question.

Consider one of the most simple implementations of the "emulated" dialogue.

First of all, we will declare the interface of the dialog box manager through which the view model will display the dialogs. To begin with, we will not take into account the possibility of receiving a “response” from the window - we simply show the dialog with the “Close” button.

 public interface IModalDialogHelper { public string Text { get; } ICommand CloseCommand { get; } void Show(string text); void Close(); } 

Next, we implement a control that will be “tied” to the manager and show the window on top of the other elements, when necessary:

 <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="Auto" /> </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> 

Again, for simplicity, this control is designed for the fact that in the view model there is an instance of the implementation of IModalDialogHelper in the DialogHelper property. In a more universal solution, it should be possible to substitute any property.

I will not give here an example of the simplest IModalDialogHelper implementation, since it is obvious: the Show () and Close () methods set IsVisible appropriately, the CloseCommand command simply calls the Close () method. Show () still sets the Text property.

It seems to be simple: call the Show () method with the desired message text, it makes the panel with the message and the button visible, then clicking the Close button sets IsVisible to its original value and the “dialog” will disappear from the screen. But there is already the first problem - the sequential display of several messages leads to the fact that the user sees only the last, since the Show () method does not expect the closure of the previous dialog.

To solve this problem, let's slightly change the prototype of the Show method:

 Task Show(string text); 

The ability to wait for the completion of this method through await gives the paste several advantages:


Here I will give one of the options for implementing IModalDialogHelper interface with asynchronous Show, which corresponds to the above points (although this implementation always returns the same modal result, it will not be difficult to make it dependent on the pressed button).

 class ModalDialogHelper : INotifyPropertyChanged, IModalDialogHelper { private readonly Queue<TaskCompletionSource<MessageBoxResult>> _waits = new Queue<TaskCompletionSource<MessageBoxResult>>(); private readonly object syncObject = new object(); private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher; //... public async Task Show(string text) { List<TaskCompletionSource<MessageBoxResult>> previousWaits; TaskCompletionSource<MessageBoxResult> currentWait; lock (syncObject) { //  ,    previousWaits = _waits.ToList(); //      currentWait = new TaskCompletionSource<MessageBoxResult>(); _waits.Enqueue(currentWait); } //  ,      foreach (var wait in previousWaits) { await wait.Task; } //        _dispatcher.Invoke(() => { Text = text; IsVisible = true; }); await currentWait.Task; } public void Close() { IsVisible = false; TaskCompletionSource<MessageBoxResult> wait; lock (syncObject) { //     wait = _waits.Dequeue(); } //            wait.SetResult(MessageBoxResult.OK); } //... } 

The basic idea behind this solution is that for each Show call, an instance of TaskCompletionSource is created. Waiting for a task created inside it will continue until the result is specified via a call to SetResult. Show, before displaying its message, waits for all the tasks that are already in the queue; after the display, it waits for its own, and Close sets the result of the current task, thereby completing it.

And a few words should be said about the use of “new” dialogs in event handlers of the CancelEventHandler type. Confirmation of actions in such events will also need to be implemented a little differently from before.

 //     ! 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; } } 

The problem is that e.Cancel will always be true for the code that caused Window_Closing, since await does not stop the execution of the stream, but creates the ability to “return” to the right place in the method after the completion of the asynchronous task. For the calling code, Windows_Closing will exit immediately after setting e.Cancel to true.

The correct solution is that the condition body should no longer operate with e.Cancel, but explicitly call the “canceled” action in such a way that it is guaranteed to be executed without additional requests, bypassing the repeated call of this handler. In the case of closing the main program window, for example, it can be an explicit call for the completion of the entire application.

Mapping performance analysis


Many developers know what a profiler is and know what tools are available for analyzing application performance and analyzing memory consumption. But in WPF applications, part of the load on the processor comes, for example, from the XAML markup processing mechanism - parsing, markup, drawing. “Standard” profilers find it difficult to determine for which activity related to XAML resources are spent.

I will not dwell on the possibilities of existing tools, just list the links to information about them. Dealing with how to use them is not difficult for any developer.



INotifyPropertyChanged


One of the most popular topics of controversy within the framework of the WPF technology is how to implement INotifyPropertyChanged in the most efficient way. The most concise option is to use AOP, as I already described in one of the examples in the article about Aspect Injector . But not everyone likes this approach, and you can use snippets as an alternative. But then the question arises about the optimal content of the snippet. First, I will give examples of not the most successful options.

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

In this case, the property name is specified as a constant, and it does not matter if it is in the named constant or, as in the example, “hardcoded” directly in the call to the alert method — the problem remains the same: if you rename the property itself, there is a chance to leave the old value of the constant. Many people solve this problem by changing the NotifyPropertyChanged method:

 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)); } } 

In this case, instead of the name, you can specify a lambda expression that returns the desired property:

 NotifyPropertyChanged(() => Name); 

Unfortunately, this option also has drawbacks - calling this method is always associated with Reflection, which is a total of hundreds of times slower than calling the previous version of NotifyProperCChanged. In the case of mobile applications, this can be critical.

In .NET 4.5, the special attribute CallerMemberNameAttribute has become available, thanks to which the first of the above problems can be solved:

 public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { //... } public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged(); } } 

If the parameter marked with this attribute is not explicitly specified, the compiler will substitute in it the name of the member of the class that calls the method. Thus, the call NotifyPropertyChanged () from the example above is equivalent to NotifyPropertyChanged (“Name”). But what to do if you need to report a change in some property “outside”, not from its setter?

For example, we have a “calculation” property:

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

When adding, removing, or changing items in the items collection, we need to report a change to TotalPrice so that the user interface always displays its current value. Given the shortcomings of the first two solutions given above, you can make the next move - still use Reflection to get the name of a property from a lambda expression, but store it in a static variable. Thus, for each individual property, a “heavy” operation will be performed only once.

 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; } } 

The very static function GetPropertyName can be put in the base class for all “notifiable” entities - it doesn’t matter. The check on UnaryExpression is needed so that the function normally processes the properties of significant types, since the compiler adds a boxing operation to bring the specified property to object.

If your project already uses C # 6.0, then the same task of obtaining the name of another property can be solved much easier - with the help of the keyword nameof. There will be no need for a static variable that remembers the name.

As a result, you can say that if the use of AOP for INotifyPropertyChanged for some reason does not suit you, you can use the following snippet:



Instead of an afterword


WPF is a good technology that Microsoft still positions as the main framework for developing desktop applications. Unfortunately, when writing programs more difficult "calculator" reveals a number of problems that are not noticeable at first glance, but they are all solvable. According to recent Microsoft statements, they are investing in technology, and there are already a lot of improvements in the new version. First of all, they relate to tools and performance. Hopefully, in the future, new features will be added not only to the tools, but also to the framework itself, facilitating the work of the programmer and eliminating the “hacks” and “bicycles” that have to be done now.

UPD: « » INotifyPropertyChanged nameof() C# 6.0

UPD2: TaskCompletionSource.

Source: https://habr.com/ru/post/262299/


All Articles