One of the things that frequently annoys me about third party
controls (including those built into the .NET Framework) are
properties that either aren't virtual
, or don't have
corresponding change events / virtual methods. Quite often I
find myself wanting to perform an action when a property is
changed, and if neither of those are present I end up having to
create a custom version of the property, and as a rule, I don't
like using the new
keyword unless there is no other
alternative.
As a result of this, whenever I add properties to my WinForm controls, I tend to ensure they have a change event, and most often they are also virtual as I have a custom code snippet to build the boilerplate. That can mean some controls have an awful lot of events (for example, the ImageBox control has (at the time of writing) 42 custom events on top of those it inherits, some for actions but the majority for properties). Many of these events will be rarely used.
As an example, here is a typical property and backing event
Quite straightforward - a backing field, a property definition, a change event, and a protected virtual method to raise the change event the "safe" way. It's an example of an event that will be rarely used, but you never know and so I continue to follow this pattern.
Despite all the years I've been writing C# code, I never
actually thought about how the C# compiler implements events,
beyond the fact that I knew it created add
and remove
methods, in a similar fashion to how a property creates get
and set
methods.
From browsing the .NET Reference Source in the past, I knew
the Control
class implemented events slightly differently to
above, but I never thought about why. I assumed it was something
they had done in .NET 1.0 and never changed with Microsoft's
mania for backwards compatibility.
I am currently just under halfway through CLR via C# by Jeffrey
Richter. It's a nicely written book, and probably would have
been of great help many years ago when I first started using C#
(and no doubt as I get through the last third of the book I'm
going to find some new goodies). As it is, I've been ploughing
through it when I hit the chapter on Events. This chapter
started off by describing how events are implemented by the CLR
and expanding on what I already knew. It then dropped the slight
bombshell that this is quite inefficient as it requires more
memory, especially for events that are never used. Given I
liberally sprinkle my WinForms controls with events and I have
lots of other classes with events, mainly custom observable
collections and classes implementing INotifyPropertyChanged
(many of those!), it's a safe bet that I'm using a goodly chunk
of ram for no good reason. And if I can save some memory "for
free" as it were... well, every little helps.
The book then continued with a description of how to explicitly
implement an event, which is how the base Control
class I
mentioned earlier does it, and why the reference source code
looked different to typical. While the functionality is
therefore clearly built into .NET, he also proposes and
demonstrates code for a custom approach which is possibly better
than the built in version.
In this article, I'm only going to cover what is built into the .NET Framework. Firstly, because I don't believe in taking someone else's written content, deleting the introductions and copyright information and them passing it off as my own work. And secondly, as I'm going to start using this approach with my myriad libraries of WinForm controls, their base implementations already have this built in, so I just need to bolt my bits on top of it.
How big is my class?
Before I made any changes to my code, I decided I wanted to know
how much memory the ImageBox
control required. (Not that I
doubted Jeffrey, but it doesn't hurt to be cautious, especially
given the mountain of work this will entail if I start
converting all my existing code). There isn't really a simple
way of getting the size of an object, but this post on
Stack Overflow (where else!) has one method.
When running this code in the current version of the ImageBox
,
I get a value of 968. It's a fairly meaningless number, but
does give me something to compare. However, as I didn't quite
trust it I also profiled the demo program with a memory
profiler. After profiling, dotMemory also showed the size of
the ImageBox control to be 968 bytes. Lucky me.
Explicitly implementing an event
At the start of the article, I showed a typical compiler generated event. Now I'm going to explicitly implement it. This is done by using a proxy class to store the event delegates. So instead of having delegates automatically created for each event, they will only be created when explicitly binding the event. This is where Jeffrey prefers a custom approach, but I'm going to stick with the class provided by the .NET Framework, the EventHandlerList class.
As the proxy class is essentially a dictionary, we need a key to identify the event. As we're trying to save memory, we create a static object which will be used for all occurrences of this event, no matter how many instances of our component are created.
Next, we need to implement the add
and remove
accessors of
the event ourselves
As you can see, the definition is the same, but now we have
created add
and remove
accessors which call either the
AddHandler
or RemoveHandler
methods of a per-instance
EventHandlerList
component, using the key we defined earlier,
and of course the delegate value to add or remove.
In a WinForm's control, this is automatically provided via the protected
Events
property. If you're explicitly implementing events in a class which doesn't offer this functionality, you'll need to create and manage an instance of theEventHandlerList
class yourself
Finally, when it's time to invoke the method, we need to
retrieve the delegate from the EventHandlerList
, once again
with our event key, and if it isn't null, invoke it as normal.
There are no generic overloads, so you'll need to cast the
returned Delegate
into the appropriate EventHandler
,
EventHandler<T>
or custom delegate.
Simple enough, and you can easily have a code snippet do all the grunt work. The pain will come from if you decide to convert existing code.
Does this break anything?
No. You're only changing the implementation, not how other components interact with your events. You won't need to make any code changes to any code that interacts with your updated component, and possibly won't even need to recompile the other code (strong naming and binding issues aside!).
In other words, unless you do something daft like change your the visibility of your event, or accidentally rename it, explicitly implementing a previously implicitly defined event is not a breaking change.
How big is my class, redux
I modified the ImageBox
control (you can see the changed
version on this branch in GitHub) so that all the events
were explicitly implemented. After running the new version of
the code through the memory profiler / magic unsafe code, the
size of the ImageBox
is now 632 bytes, knocking nearly a third
of the size off. No magic bullet, and isn't a full picture, but
I'll take it!
In all honesty, I don't know if this has really saved memory or
not. But I do know I have a plethora of controls with varying
numbers of events. And I know Jeffrey's CLR book is widely
touted as a rather good tome. And I know this is how Microsoft
have implemented events in the base Control
classes (possibly
elsewhere too, I haven't looked). So with all these "I knows", I
also know I'm going to have all new events follow this pattern
in future, and I'll be retrofitting existing code when I can.
An all-you-can-eat code snippet
I love code snippets and tend to create them whenever I
have boilerplate code to implement repeatedly. In fact, most of
my snippets actually are variations of property and event
implementations, to handle things like properties with change
events, or properties in classes that implement
INotifyPropertyChanged
and other similar scenarios. I have now
retired my venerable basic property-with-event and
standalone-event snippets with new versions that do explicit
event implementing. As I haven't prepared a demonstration
program for this article, I instead present this code snippet
for generating properties with backing events - I hope someone
finds them as useful as I do.
Update History
- 2016-05-20 - First published
- 2020-11-21 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?