UPDATE: Since writing this article, .NET 6.0 has been released, and I can confirm that all the methods described in this article continue to work perfectly well with this new version.
Whenever I’ve worked on a corporate WPF application (created and developed initially long before I showed up at the job) it was almost guaranteed that none of the views would ever work properly at design-time. This was typically due to a combination of poor design, as well as a lack of effort in ensuring the project was amenable towards a design-time driven development experience. Regardless of whatever the story is, the attitude of most of the people I’ve worked with towards design-time data has been one of pure apathy.
And I couldn’t disagree more with this sentiment! Whenever I’m working on my own projects, or happen to have control over something from the start at the workplace, I always make sure my views are design-time friendly. It just saves so much time, being able to see what the view looks like without having to compile and run; indeed, much time is saved in the creation of a new, complicated views when I can easily plug in sample data.
Unfortunately, as I discovered upon beginning to work with .NET 5.0, the port of WPF to (what was once referred to as) .NET Core sadly comes without much of its design-time bits working properly. In order to satisfy my needs, I had to figure out what still worked, as well as even do a bit of hackery to fully dot all my i’s and cross all my t’s.
I’m going to talk about the design-time approaches that (at the time of writing, with .NET 5.0) don’t work, approaches that do work (but suck) and finally: what I like the best. Some of the code examples and images of views appearing in this article are from the new Omnified application Vision — which I may have debuted already on stream by the time you’ve read this.
d:DesignData Is Basically Unimplemented
My favorite way of wiring up design-time data to a WPF view (before switching to .NET 5.0, at least) was through the the use of the d:DesignData
markup extension, used in conjunction with the d:DataContext
attached property.
This lovely little markup extension allowed you to specify a URI pointing to a XAML file containing sample data (typically within the root element of our view’s XAML itself), and we’d have sample data appearing in our designer view auto-magically.
Applying d:DesignData to an Element
d:DataContext={d:DesignData /DesignTime/SampleViewData.xaml}
Everything we needed to populate our bindings at design-time could then be defined in this particular XAML file, which I thought was quite nice as it allowed me to keep design-time data separate from the View’s actual XAML definition. It wasn’t perfect, however, as I often had to re-compile in order to get changes made to said file to propagate to the view, but that’s neither here nor there.
SampleViewData.xaml
<vm:SampleViewModel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:SomeProject.ViewModels" SomeProperty="Hello" AnotherProperty="Hello again" />
All of this let me have nice and complicated sets of design-time data, which often would need to contain collections of other sample view models (hell, collection views are when you most need design-time data). And all separate from the actual view, keeping that nice and clean.
Well, if you liked using d:DesignData
in the past: sorry, but it doesn’t exist in .NET 5.0! Well, it does (as in the markup extension is still there), but it doesn’t do anything. One big hint that might’ve lead us to this conclusion before even trying it out is the fact that the Data tab is missing in Blend when working with a WPF .NET 5.0 project.
I saw that, crossed my fingers, and tried using d:DesignData
anyway: no dice. And no, it isn’t just me: the Data panel only works with the .NET Framework. Unsurprisingly, no support for the Data panel means no support for d:DesignData
.
So, that’s a complete no go.
d:DesignInstance Is As Useless as Ever
Well, an alternative that does work with .NET 5.0 is the d:DesignInstance
markup extension. This is used to provide a value to d:DataContext
as well, however this time around we just provide an object type to initialize.
Applying d:DesignInstance to an Element
d:DataContext={d:DesignInstance Type={x:Type vm:SampleViewModel}, IsDesignTimeCreatable=True}
That’s it! We’ll have an instance of SampleViewModel
initialized and providing values to our bindings in our view at design-time.
Completely useless.
If it works for you, great! However, what good is a freshly initialized data context object? Yes, as you may have noticed, there’s no way at all to actually set the various properties that belong to our design-time data type when using d:DesignInstance
.
I’ve never written a view model that has had really any meaningful data exposed prior to actually having a model item, or business object, bound to it. Truly, the only way that I could see us making using of d:DesignInstance
is either by adding logic to the view model so that it populates itself with bunk data during design-time, or by actually creating and then using a design-time-specifically derived type of said view model.
Both of these solutions are pretty unfortunate, to say the least. I don’t want any kind of design-time data dirtying up my actual view model code (it would turn into a mess), and I don’t want to have to engage in the dependency property model system (which I would have to do) in order to determine whether or not I’m actually running in design-time (and how would I even get ahold of the dependency object required by DesignerProperties.GetIsInDesignMode
anyway?).
As far as the other possible solution goes: I don’t want to create separate types purposed just for returning design-time data. It’s incredibly clunky and I’d prefer not to ship assemblies jam packed with design-time data anyway (no harm in NSFW sample data then! ha ha ha….).
I’ve gone on enough about this particular alternative to providing design-time data. It’s no good! So, what else can we do?
Goodbye d:DataContext, A New Property Is in Town
Literally nothing I can feed to d:DataContext
appeases me. It first appeared that I was completely out of luck as far as .NET 5.0 and WPF design-time data goes…
And then I stumbled upon a completely different attached property, one that I never seen before, that actually seemed to solve all my first world problems: d:DesignerProperties.DataContext
. This attached property allows us to basically declare our sample design-time data using a comfortable property element syntax; and yes, of course, we can provide values to the properties of our data when doing so!
That means it is not useless! Time for some examples originating from real code.
Design-Time Data Declaration in WholeStatisticView.xaml
<badEcho:View x:Class="BadEcho.Vision.Statistics.Views.WholeStatisticView" 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" xmlns:badEcho="http://schemas.badecho.com/presentation/2022/02/xaml" xmlns:vm="clr-namespace:BadEcho.Vision.Statistics.ViewModels" mc:Ignorable="d"> <d:DesignProperties.DataContext> <vm:WholeStatisticViewModel Name="Total Damage Done" IsCritical="False" Value="29333882" /> </d:DesignProperties.DataContext>
Neat, I have complete control over the property values, and it works fantastically:
Magic! This d:DesignProperties.DataContext
attached property is very undocumented. I really can’t find mention about it anywhere, and it completely lacks any kind of IntelliSense as well.
A few downsides, I suppose, to this approach: our design-time data now has to be defined within the view itself, and we must also have a reference to a view model type, an instance of which we may be intending to bind to the view at run-time.
As far as the latter point is concerned, I used to never have any sort of reference to a view model within my views; however, I don’t believe it is a bad thing at all. Hell, you need to have some kind of reference if you want any kind of IntelliSense support when writing your property bindings. The d:DataContext
attached property enables up IntelliSense for bindings, and so does d:DesignerProperties.DataContext
.
As far as having to have the design-time data defined within the view itself: this is something that I used to prefer not to do as well (I guess because I was habitually using d:DesignData
), but it’s actually rather nice as we can tweak sample values and immediately see the change in the designer.
I must say, actually, that I much prefer this method to providing design-time data over anything I’ve ever used before!
There’s a Slight Problem With All of This
When everything seems to be going perfect in the world of software development, our hackles should already be rising (well, if we were cats or dogs that is), as nothing stays perfect in this world for long (at least, not without a great deal of effort and wisdom).
Indeed, using d:DesignerProperties.DataContext
will give you beautifully and perfectly functioning design-time data at first; however, you may begin to notice a problem when you begin to deal with views that are bound to collections of view models (and are thus, themselves collections of views). And, don’t forget, collection views are when having design-time data at hand really becomes important.
Let’s look at my StatisticsView
control as an example. It’s job is to display a collection of individual statistic views. Here’s our design-time data:
Design-Time Data Declaration in StatisticsView.xaml
<badEcho:View x:Class="BadEcho.Vision.Statistics.Views.StatisticsView" 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" xmlns:badEcho="http://schemas.badecho.com/presentation/2022/02/xaml" xmlns:vm="clr-namespace:BadEcho.Vision.Statistics.ViewModels" xmlns:s="clr-namespace:BadEcho.Vision.Statistics" mc:Ignorable="d"> <d:DesignProperties.DataContext> <vm:StatisticsViewModel> <vm:StatisticsViewModel.Children> <vm:FractionalStatisticViewModel Name="Player Health" CurrentValue="86" MaximumValue="1000" /> <vm:WholeStatisticViewModel Name="Level" Value="10" /> <vm:WholeStatisticViewModel Name="Lives Saved" Value="1000000000" /> <vm:WholeStatisticViewModel Name="Total Damage Received" IsCritical="False" Value="29333882" /> <vm:CoordinateStatisticViewModel Name="Coordinates" X="-1566.7635498047" Y="0.10857523977757" Z="1997.649169219" /> </vm:StatisticsViewModel.Children> </vm:StatisticsViewModel> </d:DesignProperties.DataContext>
Cool! We have a bunch of different types of statistic view models here, including several instances of WholeStatisticsViewModel
, each populated with very different and important statistics that I’m sure are oh-so-interesting to everyone who would be watching Vision in action on one of my streams.
So what do we see in the designer window?
Yeah. That’s not right. Instead of having different WholeStatisticView
instances whose properties match what I set in the design-time data portion of StatisticView
show up, we see three identical “Total Damage Done” statistics.
What’s going on here?
Design-Time DataContext Inheritance Woes
If you look at our design-time data context declaration again, the fact that none of the entries are for a statistic named “Total Damage Done” should stick out like a sore thumb. Where is the last place we did see “Total Damage Done”?
It was in the original design-time declaration for the WholeStatisticsView
control itself (scroll up if that went over your head). Yes, it would appear that a control’s design-time data context will override any other design-time data context we attempt to set for the control in places that are external to the control’s own definition (i.e., places where we wish to consume said control, like in a collection view).
Well this sucks. For awhile, I simply accepted this problem as it was and lived with it. I mean, what could I, a mere mortal, do to remedy something so deeply ingrained in the mighty and out of reach world that the particular Microsoft code responsible for this problem probably lives in?
So, I just shrugged and tolerated this issue. But, as things started to become more complicated and advanced in terms of view design with my Vision application, it suddenly became something I had to fix.
And fix it I did. First of all, let’s try to understand how exactly this problem is happening. In order to find out, I attached a dependency property change handler to the view’s metadata for the DataContext
property so I could see what was happening exactly. I recorded the following sequence of events (while attached to the WpfSurface.exe process):
- Control (
WholeStatisticView
in this case) is provided with the design-time data context we defined in the external control (StatisticsView
). Great!- As a side note: the internal
NewValueSource
property, found in theDependencyPropertyChangedEventArgs
class, had a value of Inherited.
- As a side note: the internal
- Control is then provided with the design-time data that was defined inside the control’s own XAML file, overwriting the values we want to use with those boring, same-old stock values.
- As another side note: the same internal property mentioned above had, this time around, a value of Local.
- And that’s it.
What appeared to be happening was the supersedence of inherited design-time data contexts with data contexts defined locally to the control.
It’s a Bit More Complicated Than That
I was actually looking at a more involved view when I began to feel compelled to fix this. The view I speak of is the StatisticGroupView
, which itself contains its very own a StatisticsView
(and is something that offers some really powerful functionality for which I definitely relied on design-time data to implement).
Let’s take a look at the design-time data context used for that.
Design-Time Data Declaration in StatisticGroupView.xaml
<badEcho:View x:Class="BadEcho.Vision.Statistics.Views.StatisticGroupView" 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" xmlns:badEcho="http://schemas.badecho.com/presentation/2022/02/xaml" xmlns:vm="clr-namespace:BadEcho.Vision.Statistics.ViewModels" xmlns:v="clr-namespace:BadEcho.Vision.Statistics.Views" xmlns:s="clr-namespace:BadEcho.Vision.Statistics" mc:Ignorable="d"> <d:DesignProperties.DataContext> <vm:StatisticGroupViewModel Name="Damage Taken"> <vm:StatisticGroupViewModel.Statistics> <vm:StatisticsViewModel> <vm:StatisticsViewModel.Children> <vm:WholeStatisticViewModel Name="Last" Value="3031"/> <vm:WholeStatisticViewModel Name="Max" Value="293939339"/> </vm:StatisticsViewModel.Children> </vm:StatisticsViewModel> </vm:StatisticGroupViewModel.Statistics> </vm:StatisticGroupViewModel> </d:DesignProperties.DataContext>
I created a dependency property change handler for the base type for all these views (View
, essentially), and observed the follow sequence of events:
- The first data context change is for the
StatisticsView
, however, the data context is notStatisticsViewModel
, but ratherStatisticGroupViewModel
. This seems weird at first, but it makes sense as that is the data context for theStatisticView
‘s parent, and theDataContext
property, courtesy of WPF’s dependency property inheritance system, is inherited by children.- The
NewValueSource
for this data context is Inherited.
- The
- The next data context change that occurs is to the very same
StatisticsView
, this time with a data context that is the correct type:StatisticsViewModel
. Again, this all makes sense: the parent data context is inherited initially, before ending up being replaced by what we set in our design-time data context.- The
NewValueSource
for this data context is Local.
- The
- The
WholeStatisticView
data contexts are next. The first control receives the correct data context that we defined inStatisticGroupView
.- The
NewValueSource
for this data context is Inherited.
- The
- The same
WholeStatisticView
now gets updated with the wrong data context: the one we defined in theWholeStatisticView
control itself.- The
NewValueSource
for this data context is Local.
- The
- This repeats again for the next
WholeStatisticView
.
Alright! We got a good handle on what’s going on here. But, it appears that gaining greater understanding to the problem only muddies our potential solutions more.
At first glance, it would appear that the way to prevent Local sourced data context values from overriding our preferred Inherited data context values is to simply revert the data context to the previously bound one if a new one comes along with a NewValueSource
of Local.
That won’t work, however. If we do that, then we’ll be blocking that very needed update that occurs during step #2, where the data context for StatisticsView
, originally inherited from its parent, gets replaced with the correct one that corresponds to our our d:DesignerProperties.DataContext
declaration.
That very necessary update unfortunately happens to also be Local. If we block it, then the data context changes we see in #3 and #4 and beyond won’t even occur — the WholeStatisticView
controls will end up with no data context at all.
Here’s the Fix for a Perfect Design-Time Support
Luckily for all of you (and, mainly me), after looking at this for a bit I devised a rather simple way to allow for us to have beautifully functioning design-time support with WPF in .NET 5.0.
Forget all of the internal NewValueSource
property mumbo jumbo. No need to engage in any dirty Reflection here! It’s actually quite simple, and all I needed to do was engage in a tiny bit of coercion.
I like to have a base type for all of my views, and I hope you do too; the reasons I have one (it derives from UserControl
) are many and varied, and we have here yet another reason for having one. Inside this base type (View.cs), I added a coerce value callback for the DataContext
property.
Inside this coercion callback for DataContext
, we look at a few things:
- First, we check if we’re in design-time. If we aren’t, we get the hell out of there! None of this should happen during run-time.
- Assuming we are in design-time, we check if a previous non-null data context has been received by the view by referring to a field designated for the storage of a local design-time data context for the view.
- If no local design-time data context is present, then the new data context is set as the the local design-time data context and it also wins coercion.
- If a previously stored local data context is present, then we compare its type to the new data context’s type. If their types differ, the new data context is set as the local design-time data context instead, and wins the coercion as well. Otherwise, the previously stored local data context wins coercion.
By following this logic, our designer will be loaded with the design-time data we want it to be loaded with. After putting in this code, design-time data is working flawlessly for me. I can develop WPF interfaces in .NET 5.0 to the best of my abilities now!
Here is the code that ensures the proper design-time data context wins coercion:
Design-Time DataContext Coercion Code (Only the Relevant Bits)
/// <summary> /// Initializes the <see cref="View"/> class. /// </summary> static View() => DataContextProperty.OverrideMetadata(typeof(View), new FrameworkPropertyMetadata(null, OnCoerceDataContext)); private static object? OnCoerceDataContext(DependencyObject d, object? baseValue) { View view = (View)d; return view.EnsureLocalContext(baseValue); } private object? EnsureLocalContext(object? newContext) { if (!DesignerProperties.GetIsInDesignMode(this)) return newContext; if (_localDesignContext != null) { if (_localDesignContext.GetType() != newContext?.GetType()) _localDesignContext = newContext; return _localDesignContext; } _localDesignContext = newContext; return newContext; }
You can find more complete code examples on the Bad Echo technologies source code repository.
I understand how it may be a bit confusing that we’re ensuring that a “local” data context stay in place with EnsureLocalContext
, given that (if you scroll back up to the sequence of data context property change events) our previously stated goal was to block all data context changes with a NewValueSource
value of Local.
But, we’re not actually doing that. We’re ensuring that the design-time data context that is getting used is the design-time data context that is local in terms of where it is declared in relation to the view we’re currently working on. So, I’ve decided to eschew the (somewhat confusing) dependency property inheritance jargon here and use something more practical instead.
Design-time data works great for me now. Hopefully it’ll work great for you, and hopefully you learned something! I did.
Until next time.