Introduction
In a nutshell, the code which I will be discussing in this article will enable your application to gracefully degrade the brush values that are used on the elements in an application, effectively useful in situations where performance is more important than looks.
What the resulting code achieves is, upon specifying a Primary and Secondary brush, it will selectively apply this brush. So in our problem, when a user connects to a client remotely, the application will be notified and the Secondary brush will be used. Ideally, the goal is to have the Secondary brush to be a really light weight brush (SolidColorBrush
).
Included with this article is also a sample application for you to try out.
Background
There are times when you might want to have your brush gracefully degrade; a common scenario would be if you're remoting into a client and running a WPF application that heavily uses gradients. This can dramatically degrade the experience of using your application remotely, as gradients will be rendered into bitmaps and transferred over, which is a big bandwidth hog.
Using the Code
We can utilise this custom brush in two ways:
- Property Element syntax
- Markup Extension syntax
Property Element Syntax
<Button.Background>
<c:RemoteBrush>
<c:RemoteBrush.PrimaryBrush>
<SolidColorBrush Color="Yellow"></SolidColorBrush>
</c:RemoteBrush.PrimaryBrush>
<c:RemoteBrush.SecondaryBrush>
<LinearGradientBrush>
<GradientStop Color="Red" Offset="0"></GradientStop>
<GradientStop Color="Green" Offset="0.9"></GradientStop>
</LinearGradientBrush>
</c:RemoteBrush.SecondaryBrush>
</c:RemoteBrush>
</Button.Background>
Markup Extension Syntax
<Button Background="{c:RemoteBrush PrimaryBrush=Pink, SecondaryBrush=Orange}" />
That's pretty much it, very simple and terse.
Points of Interest
There are lots of interesting things that I've learned about while implementing this custom brush.
Firstly, the natural answer for most would be to inherit from Brush
. That is the ideal answer, but the problem is that Brush
is marked as abstract
, and there are some abstract members that need to be implemented, however, they are marked as internal
. So again, that shot me right in the foot.
Another way that could have been done was to achieve it through the combination of Styles and Triggers. That could very well work in some ways, but it would be a lot more messier for what I aimed to achieve.
Finally, the next best answer was to use MarkupExtensions. It was easy to implement, and it provided me the opportunity to achieve the syntax which I wanted. However, there were some interesting quirks around using MarkupExtensions, that I discovered.
The main logic to our problem really can be summarized in a few steps:
- Listen to session change events.
- Upon remote session connect and disconnect, fire off an event.
- Within the event handler, we need to refresh the property's value.
That seems fairly trivial. Not quite. The problem with MarkupExtensions is that once they are evaluated once, they aren't evaluated again. Major problem. There's no direct way to force it to refresh. At least, there wasn't some clean way to achieve it.
The next best answer that came up was to return a Binding
. I think this was probably the only way to overcome this issue (please tell me if there's a better way).
The second dilemma actually stems to be the Binding
object itself. As most of you who have done binding through procedural code knows, you would normally set the Path
and Source
, and then call SetBinding
on the target, etc. Well, here's the issue, once you set the Path
and the Binding
is consumed, you can't change the Path
anymore. So how do I toggle between two different brushes on the bound property...
Binding Converters to the rescue! Yes, it's definitely not elegant, but it sure does the job pretty well. One thing is for sure, you can explicitly update/refresh a binding, and it will call the Convert
method on the Converter. So the only bit of coupling we have is between our Markup Extension and our Binding Converter.
There's a quirk I'd like to highlight, and that's when you decide to use the markup extension syntax and do nested extensions:
<Grid Background="{c:RemoteBrush PrimaryBrush=Blue,
SecondaryBrush={StaticResource myBrush}}" />
That's actually valid, however, Visual Studio thinks otherwise. Philipp Sumi has highlighted this in a blog post already: Nested Markup Extension Bug.
This bug exists in Visual Studio 2008 as well as Visual Studio 2010 BETA 2 at the time of writing, I've submitted a bug report at connect: WPF Nested Markup Extension Bug.
To go around this issue, you can apply {StaticResource...}
on the property element syntax:
<Button.Background>
<c:RemoteBrush SecondaryBrush="{StaticResource myBrush}">
<c:RemoteBrush.PrimaryBrush>
<SolidColorBrush Color="Yellow"></SolidColorBrush>
</c:RemoteBrush.PrimaryBrush>
</c:RemoteBrush>
</Button.Background>
Originally, I had the logic of detecting connected remote sessions baked in the class, but I thought, I can go one step further and make it a more flexible FallbackBrush
, which allows inheritors to provide their own logic to determine when to toggle the brushes. I can definitely think of a few other conditions you might want to do this:
- Detect your hardware capabilities and gracefully degrade as well, i.e., check the
RenderTier
value.
- Provide a visual cue based on an external event.
The possibilities are endless.
So without further ado, here's the actual abstract class that will concern you the most:
[MarkupExtensionReturnType(typeof(object))]
public abstract class FallbackBrush : MarkupExtension{
#region Instance Variables
private DependencyObject _baseObject;
private DependencyProperty _baseProperty;
#endregion
#region Public Properties
public Brush PrimaryBrush { get; set; }
public Brush SecondaryBrush { get; set; }
#endregion
#region Implementation
protected void RefreshValue(){
if (_baseObject != null && _baseProperty != null)
{
var bindingExpression =
BindingOperations.GetBindingExpressionBase(_baseObject, _baseProperty);
if (bindingExpression != null)
bindingExpression.UpdateTarget();
}
}
public override object ProvideValue(IServiceProvider serviceProvider){
IProvideValueTarget valueProvider =
serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (valueProvider != null)
{
this._baseProperty = valueProvider.TargetProperty as DependencyProperty;
this._baseObject = valueProvider.TargetObject as DependencyObject;
}
var brushBinding = new Binding() { Converter = new FallbackBrushConverter(this) };
return brushBinding.ProvideValue(serviceProvider);
}
#endregion
#region Abstract Methods
public abstract bool UseSecondaryBrush();
#endregion
internal class FallbackBrushConverter : IValueConverter
{
private FallbackBrush _fallbackBrush;
public FallbackBrushConverter(FallbackBrush extension)
{ _fallbackBrush = extension; }
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture){
return _fallbackBrush.UseSecondaryBrush() ?
_fallbackBrush.PrimaryBrush : _fallbackBrush.SecondaryBrush;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{ throw new NotImplementedException(); }
}
}
Note: Yes, I did sort of violate the convention with not appending Extension at the end of my class name, but that's really just to mitigate typing and give it a cleaner feel when using the Property Element syntax (so please don't shoot me!).
All you need to do in your derived class is to implement the abstract method called UseSecondaryBrush
.
Here's what the remote brush looks like:
public class RemoteBrush : FallbackBrush
{
public RemoteBrush()
{
SystemEvents.SessionSwitch +=
new SessionSwitchEventHandler(SystemEvents_SessionSwitch);
}
public bool IsRemoteSession { get; set; }
void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
switch (e.Reason)
{
case SessionSwitchReason.RemoteConnect:
this.IsRemoteSession = true;
this.RefreshValue();
break;
case SessionSwitchReason.RemoteDisconnect:
this.IsRemoteSession = false;
this.RefreshValue();
break;
}
}
public override bool UseSecondaryBrush()
{
return this.IsRemoteSession;
}
}
As you can see, in order for the binding to refresh, you need to explicitly call RefreshValue
; upon calling RefreshValue
, the underlying converter will invoke the UseSecondaryBrush
abstract method that you've implemented.
History
- November 9th 2009 - First revision out.