It is 2018, the time of .NET Core, x-plat, clouds, microservices, blockchain and shine of JavaScript. But, there are guys, like me, who still maintain and sometimes develop classic .NET desktop applications for Windows.
I am not a WPF expert, but I spent a couple of days reviewing, testing and fixing one of our desktop apps and I definitely learned a couple of new tips & tricks that worth to share with other non-experts.
Part #1: General Tips
Tip #1.1: Choose right library/framework
It happened that we use MvvmLight. The library is lightweight and already exists for almost 10 years, MVVM pattern is well-known and lets us keep solution code reasonably well-structured.
But this is definitely not the only choice, there are many other different-purpose libraries and frameworks that may suit you better, especially if you do green-field development. So choose carefully:
Tip #1.2: Distribute using Squirrel.Windows
Installers always were hard, the seamless auto-update process is even harder. But, today, we have the solution that works for simple user-oriented applications that don’t do crazy things during installation. This solution is Squirrel.Windows – an installation and update framework for Windows desktop apps, designed for C# apps.
It’s definitely worth to learn it once and use it for all apps that you develop.
Tip #1.3: Think about monitoring
Aggregated analytics from user’s machine is priceless for successful apps. There are plenty amount of data that can help you deliver better apps:
- Crash reports
- Application version distribution
- User’s count / Active user
- Performance / Integrations tracking
- Custom events / Logs
It is not always possible to collect all kinds of data from user’s machine, but do it if you can. There are a couple of services that may help you, like Application Insights, HockeyApp and others.
Tip #1.4: Use the full power of IDE
Learn tools that MS baked for you and use them
- Inspect XAML properties while debugging
- How to: Use the WPF Tree Visualizer
- How to: Display WPF Trace Information
- UI Performance Analysis Tool for WPF Applications
- WPF Performance Suite
- Creating a UI by using Blend for Visual Studio
Tip #1.5: Debug Data Binding Issues
When data bindings do not play nice you have a possibility to debug. It is not super intuitive, but there are ways to step into the binding process to better figure out what is actually going on. Check this nice article from Mike Woelmer – How To Debug Data Binding Issues in WPF
Part #2: MVVM Light – Code Tips
C# quickly evolves over time, more and more features become available to us. It is not always obvious how to use new async code with an old API.
Tip #2.1: “New” INotifyPropertyChanged syntax
I think almost any WPF developer knows how to implement INotifyPropertyChanged interface
public class MyViewModel : INotifyPropertyChanged { private string _isBusy; public event PropertyChangedEventHandler PropertyChanged; public MyViewModel() {} public string IsBusy { get { return _isBusy; } set { _isBusy = value; OnPropertyChanged("IsBusy"); } } protected void OnPropertyChanged(string name) { if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs(name)) } }
Using MVVM Light you can do way shorter (such syntax probably exists for a while, but I discovered it only recently)
public class MyViewModel : ViewModelBase { public MyViewModel() {} private string _isBusy; public string IsBusy { get => _isBusy; set { Set(() => IsBusy, ref _isBusy, value) } } }
All property change events will happen under the hood of Set method. Also, Set method returns true when the value changed so you can use it to do additional actions on property change.
private string _isBusy; public string IsBusy { get => _isBusy; set { if (Set(() => IsBusy, ref _isBusy, value)) { // Do whatever you need on update } } }
There are overloads that allow us to omit first argument – propertyExpression. In this case [CallerMemberName] will be used as the property name, so the code will be even shorter. Not bad for 2018 =)
private string _isBusy; public string IsBusy { get => _isBusy; set => Set(ref _isBusy, value); }
Tip #2.2: Async to Action glue
C# async was designed to be better compatible with old APIs and consume Action or delegate. Also, it is one of the reasons why async void exists in the language, but we should always use async Task in our own code.
Two following casts are valid
Action task = async () => await Task.Yield(); Func task2 = async () => await Task.Yield();
Read “Do async lambdas return Tasks?” to better understand what’s actually going on here. It means that you can pass your async method as Action to RelayCommand.
new RelayCommand(async() => await Download());
TBH, you should use it like this (explanation in the next tip)
new RelayCommand(async() => await Download(), keepTargetAlive:true);
Tip #2.3: Do not use lambdas with RelayCommand
Lambdas as a parameter for RelayCommand is a bad idea unless you know what can go wrong and use the latest version of MvvmLight.
Actually, I have spent almost 2 days of my life to figure out why at some point of time several buttons in our application stopped working, even though all commands defined in the ViewModel are read-only and assigned once in the constructor.
We had simple commands that do some trivial actions on click, so the developer decided to use lambda in command declaration to save space and simplify the code.
new RelayCommand(() => IsBusy = true);
The code looks simple and correct, but RelayCommand under the hood stores only weak reference to the delegate and any GC cycle can recycle local lambda function. So at some point in time (after next cycle of GC) RelayCommand may not find delegate to call and nothing will happen after the click. For a deeper analysis of this behavior, you can read “RelayCommands and WeakFuncs“.
At the time of writing this post, the issues in MvvmLight library were fixed (Using RelayCommand and Messenger (and WeakAction) with closures) and released in version 5.4.1. But fix does not apply by default.
If you really want to use lambdas with RelayCommand & Messenger you should manually set keepTargetAlive:true (false by default), but probably better do not use them at all.
new RelayCommand(() => IsBusy = true, keepTargetAlive:true);
P.S. Worth to mention that Laurent Bugnion has the course on Pluralsight “MVVM Light Toolkit Fundamentals” that provides detailed MVVM Light overview.