Scott Hanselman

Back to Basics - This is not the object you're looking...wait, oh, it is the object

July 2, '08 Comments [27] Posted in Back to Basics | Learning .NET | Windows Client
Sponsored By

Downcasting is kind of something you usually want to avoid, but sometimes it's not easily avoided. It depends on the situation. Because it's not an idiom you'll find yourself doing every day, sometimes you'll forget to do it entirely and what you're looking for is right there under your nose.

The Problem

A buddy was trying to host the WebBrowser control in his WinForms application. It was a locally run application that was using the WebBrowser control to generate part of the UI. The user might click on a link, and you'd want to do something based on the click of an link/anchor in the HTML.

In this trivial example, I add the words "Add Comment" to a page I've navigated to within my WinForms Application.

image

First, we make some new elements within the WebBrowser control itself, like this.

HtmlElement div = webBrowser1.Document.CreateElement("div");
div.SetAttribute("style", "color:blue;");
webBrowser1.Document.Body.AppendChild(div);

HtmlElement anchor = webBrowser1.Document.CreateElement("a");
anchor.InnerText = "Add Comment";
anchor.Id = "lnkAddComment";
anchor.SetAttribute("href", "#");
anchor.Click += new HtmlElementEventHandler(doit);
div.AppendChild(anchor);

Notice that we can hook up a managed event handler to the anchor, and that the anchor's href attribute points to nothing, by setting it to "#".

anchor.Click += new HtmlElementEventHandler(doit);

My friend hooked up the event to doit() that had a standard EventHandler signature like this:

public void doit(object sender, HtmlElementEventArgs e)

You've likely seen this "object sender, SomeEventArgs e" method signature before. He started digging around in the HtmlElementEventArgs object, trying to find something he could use to known if the user had clicked on the link called lnkAddComment. He was bummed to find nothing he could use.

image 

There's a couple of thing to learn here. First, just hovering over the sender object in the debugger shows us a lot. We can see that the type of sender isn't just object, but rather System.Windows.Forms.HtmlElement. This is a CLR type, and not a JavaScript type or a type you'd know about if you were familiar with the DOM and were expecting something more DOM-like.

The DOM element in this case, is there, but it's a COM Object and is part of the IE DOM object model and you'd be better off using Visual Basic if you care deeply about getting into it as we've recently learned.

image

Downcast, Damned Spot!

However, it's an HtmlElement and it has and id property with the string "lnkAddComment," and that's useful. At this point one could down-cast it. That means, telling the compiler that we know more than it does and that we're totally sure and are willing to risk our necks.

HtmlElement foo = (HtmlElement)sender;
if (foo.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}

There's some interesting things we can think about here. First, what if we're wrong? Well, bad things for one:

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.InvalidCastException: Unable to cast object of type 'Something.You.Did.Not.Plan.On' to type 'System.Windows.Forms.HtmlElement'.
   at WindowsFormsApplication1.Form1.doit(Object sender, HtmlElementEventArgs e) in C:\Yay\Form1.cs:line 32

And that sucks. However, in this case we are pretty darn sure, but we should be more defensive about it.

public void doit(object sender, HtmlElementEventArgs e)
{
if (sender is HtmlElement)
{
HtmlElement foo = (HtmlElement)sender;
if (foo != null && foo.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}
}
}

But that is kind of verbose, we can try a defensive cast:

HtmlElement foo = sender as HtmlElement;
if (foo != null && foo.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}

In this case, if sender isn't an HtmlElement, we'll get back null. The same code in VB is pretty clear also. See the TryCast?

Public Sub doit(ByVal sender As Object, ByVal e As HtmlElementEventArgs)
Dim foo As HtmlElement = TryCast(sender,HtmlElement)
If ((Not foo Is Nothing) AndAlso (foo.Id = "lnkAddComment")) Then
MessageBox.Show("woot")
End If
End Sub

The IL (Intermediate Language) instructions are interesting also as it's asking explicitly if that object is what I think it is:

L_0001: ldarg.1 
L_0002: isinst [System.Windows.Forms]System.Windows.Forms.HtmlElement

I suppose that means I could just ask myself, but that's largely stylistic as the resulting IL is virtually the same, with the defensive cast being a simpler.

But Why the Downcast at All?

At this point you might be asking, Why did I have to downcast at all? Doesn't that mean the EventHandler pattern is lame? Maybe, but here's the thinking as described by Jeffrey Richter on page 228 of his most excellent CLR via C#, 2nd Edition (with [contextual edits and emphasis] by me so it makes sense in this post).

A lot of people wonder why the event pattern requires the sender parameter to always be of type Object. After all, since the [HtmlElement] will be the only type raising an event with a [HtmlElementEventArgs] object, it makes more sense for the callback method to be prototyped like this:

void MethodName(HtmlElement sender, HtmlElementEventArgs e);

The pattern requires the sender parameter to be of type Object mostly because of inheritance. What if [HtmlElement] were used as a base class for [DerivedHtmlElemen]? In this case, the calIback method should have the sender parameter prototyped as an [DerivedHtmlElement] instead of HtmlElement, but this can't happen because [DerivedHtmlElement] just inherited the Click event. So the code that was expecting an [DerivedHtmlElement] to raise the event must still have to cast the sender argument to an [DerivedHtmlElement]. In other words, the cast is still required, so the sender parameter might as well be typed as Object.

The next reason for typing the sender parameter as Object is just flexibility. It allows the delegate to be used by multiple types that offer an event that passes a [HtmlElementEventArgs] object. For example, a [PopHtmlElement] class could use the delegate even if this class were not derived from [HtmlElement].

One more thing: the event pattern also requires that the delegate definition and the callback method name the EventArgs-derived parameter e. The only reason for this is to add additional consistency to the pattern, making it easier for developers to learn and implement the pattern. Tools that spit out source code (such as Microsoft Visual Studio) also know to call the parameter e.

If you're interested in this kind of stuff, you should totally buy his book.

Can we avoid the cast completely?

Why did we downcast in the first place? We wanted to get ahold of the HtmlElement.Id property so we could do a string comparison in order to tell if an object is the one we're looking for. Perhaps we can check for that object's identity using something a little cleaner than a string.

In this case, since we were the ones that created the anchor in the first place we can check the "object sender" against a saved reference to our anchor by checking object identity. Are these the same two objects? Is the object I added to the object model the same one that is coming back to me as the sender parameter to this Event Handler?

HtmlElement anchor;
public void doit(object sender, HtmlElementEventArgs e)
{
if (sender.Equals(anchor))
{
MessageBox.Show("woot");
}
}

Is this a good idea? What about using == instead? In this case, I CAN use == because HtmlElement has explicitly created equality operators, so when I'm using == to compare these two instances I'm ACTUALLY calling a static op_Equality(HtmlElement, HtmlElement) on the HtmlElement type. It's static because both side might be null and I can't call methods on null instances.

HtmlElement anchor;
public void doit(object sender, HtmlElementEventArgs e)
{
if (anchor == sender)
{
MessageBox.Show("woot");
}
}

However, while operator overloading is common in C++ it's generally considered to be unnecessarily obscure in C# and moreover, you just can't count on folks to be consistent. For example, when I say == do I mean value equality or reference equality? In the case of HtmlElement they mean reference equality. In C++ an overloaded == is usually done for deep value comparisons.

I will get this warning if I try anyway:

Possible unintended reference comparison; to get a value comparison, cast the right hand side to type 'System.Windows.Forms.HtmlElement' 

I like to use Equals() myself for clarity's sake, but touché! It's going to just do this internally anyway:

public override bool Equals(object obj)
{
return (this == (obj as HtmlElement));
}

Commenter Adam makes the excellent point that using Object.ReferenceEquals is explicitly clear in expressing intent. The result would be:

public void doit(object sender, HtmlElementEventArgs e)
{
if (Object.ReferenceEquals(anchor,sender))
{
MessageBox.Show("woot");
}
}

Of course, the implementation of ReferenceEquals is just this again. ;)

public static bool ReferenceEquals(object objA, object objB)
{
return (objA == objB);
}

What's the point?

Know your options, but above all, know your intentions and make sure that the code you're writing correctly expresses your intent.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Wednesday, July 02, 2008 2:38:29 AM UTC
Operator overloading in Java? How did you do that?
Denis
Wednesday, July 02, 2008 3:05:08 AM UTC
Oops! I meant Delphi! Fixed.
Scott Hanselman
Wednesday, July 02, 2008 3:42:59 AM UTC
hey scott, anothe great post in this series... I've seen downcasting many times in my asp.net codebase and always wondered if there was a safer / better way to do it.

really enjoy these back to basics posts and look forward to reading more.
Wednesday, July 02, 2008 4:12:41 AM UTC
This is one of the problems where lambdas might be quite handy:

HtmlElement element;
element.Click += (sender, eventArgs) =>
{
if (element.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}
}

instead of the version mentioned in the original post.
Frederik Siekmann
Wednesday, July 02, 2008 4:31:47 AM UTC
Frederik! Nice!
Scott Hanselman
Wednesday, July 02, 2008 4:50:14 AM UTC
Thx.
One big problem that may occur with lambdas:
If the element-variable gets changed after the event declaration the event handler will change also and therefore won't work properly (but thats always the problem with anonymous methods):

foreach (var element in elementList)
element.Click += (sender, eventArgs) => Debug.WriteLine(element.Tag); // element will probably always be the last element of elementList

I mention this because I stumpled across this a few times...
Frederik Siekmann
Wednesday, July 02, 2008 9:10:07 AM UTC
I'm not a C# programmer by trade, but in your defensive downcasting example would it not be better to scope the definition of foo to within the if block, since outside that scope it may or may not contain a valid reference, and by making the scope tighter, you will get a compiler error if you try to use foo later on?


public void doit(object sender, HtmlElementEventArgs e)
{
if (sender is HtmlElement)
{
HtmlElement foo = (HtmlElement)sender;
if (foo != null && foo.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}
}
}
Andy
Wednesday, July 02, 2008 9:28:37 AM UTC
"The pattern requires the sender parameter to be of type Object mostly because of inheritance. What if [HtmlElement] were used as a base class for [DerivedHtmlElement]? In this case, the calIback method should have the sender parameter prototyped as an [DerivedHtmlElement] instead of HtmlElement, but this can't happen because [DerivedHtmlElement] just inherited the Click event. So the code that was expecting an [DerivedHtmlElement] to raise the event must still have to cast the sender argument to an [DerivedHtmlElement]. In other words, the cast is still required, so the sender parameter might as well be typed as Object."

I really don't buy this.

Sure we would now have a [DerivedHtmlElement] which has a handler which takes a [HtmlElement] instead of an Object. but who cares.

The argument that "a cast is needed either way and therefore it might as well be an object" is not comprehensive.

Consider the humble button.

Wouldn't it be so much easier if the click event were to pass a sender of type Button instead of object?

Surely in 90% of cases, a cast would be to type 'Button' rather than some 'DerivedButton'

The argument seems to suggest... "since you're going to need to cast sometimes, It's best if wee make you cast in all cases"

Come On! Are you (Richter) serious?

Now lets look at the downside of this....

So now the 'DerivedButton' has a click event which passes a 'DerivedButton' disguised as a 'Button'.

So ok, you might need to cast in order to access the derived members of the 'DerivedButton' but you still don't need to cast to access the 'Button' members.

So what's the real downside to precasting?

Well what if you want to pass something that isn't a button to a button event handler?
I admit that that would be a bit harder, but in truth isn't is also a bit dumber?

In my opinion using Sender as Object is like returning to the dark old days of VB6 and Variant. It's like saying "I don't know" or "I don't care"

I would however love to hear the counter argument to this. I'm sure there is one.

Bring it on. I love being proved wrong.... It makes me smarter :D
Wednesday, July 02, 2008 12:23:59 PM UTC
@Rory: Sometimes I'll have lots of elements on my page (particularly when using repeaters and the ItemDatabound event) wired up to the same event handler. In these situations, you could have three buttons, edit, save, delete all on the same repeater. If you derive them from the button object you could then go

if (edit.equals(sender))
{
//edit
}

and so on.

Wednesday, July 02, 2008 12:59:53 PM UTC
I agree that the explanation for sender as object is very weak, and am surprised to see it coming from Richter. In fact, I'm pretty sure I heard him rant about that decision a few years ago at Devscovery.
I suspect the decision had more to do with some kind of Visual Studio tooling or Visual Basic language limitation.
Wednesday, July 02, 2008 1:43:00 PM UTC
The sender as object has always suggested to me that the only use for it is as a reference equality test. If you need to grab something from the instance you can either keep a reference to it as Scott has done, or make sure it's in the WhateverEventArgs if you've got control over that.
Giraffe
Wednesday, July 02, 2008 2:45:27 PM UTC
One neat thing about using "object" as the type for sender is that you can share event-handler methods between multiple owners. I've got lots of code which adds the same event-handlers to TextBoxes, MaskedTextBoxes, ComboBoxes and so on.

Once you've made a decision to go a certain way for reasons like that, I'd prefer consistency over the occasional convenience of pre-casting.

Now, on the other hand, a generic EventHandler delegate with a type parameter for the sender, well, that might be useful.
Wednesday, July 02, 2008 2:45:46 PM UTC
You miss an option.

I've always found Object.ReferenceEquals to be the better option for your "is the sender the object I'm expecting" question. You only test whether the pointed-to objects are equal and not bother with accidental or purposeful value equality (since it's not necessary).
Wednesday, July 02, 2008 2:46:15 PM UTC
Live comment preview is worthless. :) You *missed* an option!
Wednesday, July 02, 2008 4:00:36 PM UTC
@Mark - You beat me to it.

I used this on a project about a year ago:
public delegate void EventHandler<TSender, TEventArgs>(TSender sender, TEventArgs e);

(I believe that's right. I don't have access to the code where I'm at now, but I believe this is correct.)

The project was a communication tool that had a lot of encapsulated socket code. So the server class would have an event:
public event EventHandler<Server, ConnectedEventArgs> ClientConnected; //or something similar

And the handler could easily work with the Server through the sender parameter without casting.
Wednesday, July 02, 2008 4:14:04 PM UTC
Andy - You're totally right!

Adam - Yes, ReferenceEquals is very useful. I'll add it to the post!

Mark - Exactly. It always bothered me that sender was and Object, but the fact you can share the EventHandler combined with the fact that you will probably downcast at some point anyway got me mostly over it. For now. ;) I may go back and forth.

Giraffe - And indeed that's the cleanest way. The argument being that if you were supposed to know about this object, you'd have a reference to it already! ;)

Josh - It's an argument. I'm still up in the air on if it's a good one, but you can see that I like to work around that just by keeping a reference around like Giraffe commented.

Rory - I dunno...I think he's saying that something non-Button-esque is can also have a Click Event. Still, I tend to agree that it smells. And that's why I do the reference thing.
Scott Hanselman
Wednesday, July 02, 2008 4:30:22 PM UTC
RE: ReferenceEquals

The one thing to note about "what's the point" is the ReferenceEquals will forcibly use the default == implementation on Object, which is guaranteed to be reference equality. Using the == operator on your object instance directly can use an overloaded operator and you may not get expected results (perhaps another developer overrode it and you didn't realize).
Wednesday, July 02, 2008 5:14:35 PM UTC
A stylistic choice more than anything, but I find that using a defensive method for most of the event handlers leads to a lot of my code being nested one level more than it need be, making it slightly harder (for me) to read and manage.

I prefer:
public void doit(object sender, HtmlElementEventArgs e)  
{
if (!(sender is HtmlElement))
{
return;
}

HtmlElement foo = (HtmlElement)sender;
if (foo != null && foo.Id == "lnkAddComment")
{
MessageBox.Show("woot");
}
}
Wednesday, July 02, 2008 5:27:26 PM UTC
The point of object as sender is so that you can use the same handler for similar events from completely different types. This often comes up in UI programming, when you want the same handler to handle an event from unrelated controls (like a button and a menu item). It is unfortunate that a side effect of this pattern is excessive downcasting; I am undecided on whether it would have been better to strongly type the caster from the beginning, which in some cases would force extra trivial event handlers to be written.
Wednesday, July 02, 2008 6:37:54 PM UTC
Sure we would now have a [DerivedHtmlElement] which has a handler which takes a [HtmlElement] instead of an Object. but who cares.


Say you have some custom (as in not from the System.Windows.Forms namespace) control classes with the following names and inheritance realtionships: Button <- SpecialButton <- SuperSpecialButton <- CrazyButton. Button starts off with a Click event. SpecialButton adds a DoubleClick event. SuperSpecialButton adds TripleClick, and CrazyButton, just to be crazy, has Drag events.

If we follow your rule of always making the event you are creating take an argument of the type of the class you are adding it to, we have a serious issue of mixed signals. Your CrazyButton class has 4 different events all taking different types of senders.

Yes, this doesn't actually impede our ability to do whatever checks and manipulations need to be done. But the message it sends to the programmer who's going to use these classes is VERY muddy. If all they care about is CrazyButton, they are going to see 4 different events with 4 different sender types, and wonder if there's some design reason why the types are different.

Part of writing clean and maintainable code is to ensure that your code is sending the right signals to the programmer. Different sender types for different events within the same class hierarchy would be additional complexity that adds no additional features or information. Therefore a common base type should be used unless there is an additional feature or information to convey.

Hence Scott's point about intentions:
Know your options, but above all, know your intentions and make sure that the code you're writing correctly expresses your intent.
Thursday, July 03, 2008 2:49:29 AM UTC
Scott,

I might look like an idiot.

But, what is the use of checking the sender (atleast when event is raised by the link itself) and you are not calling
doit explicitly

The check is needed when inside your code, you will do doit(someOtherLink, EventArgs.Empty).
Thursday, July 03, 2008 1:37:52 PM UTC
The point of ReferenceEquals is of course to give a name to a specific type of operation.

Even though it internally uses ==, it uses it on two variables of type Object, and thus won't be swayed by operator overloading, so it'll always default to the default == comparison, reference equality.
Thursday, July 03, 2008 1:49:29 PM UTC
Perhaps I'm dense here and just giving you all a chance to tell me why I'm such a newb, but wouldn't specifically typed delegates still be usable with general (shared) event handlers?

Consider this code:

public class A { }
public class B : A { }
public class C : A { }

public delegate void ADelegate(A sender, EventArgs e);
public delegate void BDelegate(B sender, EventArgs e);
public delegate void CDelegate(C sender, EventArgs e);

class Program
{
static void Main(string[] args)
{
A a = new A();
B b = new B();
C c = new C();

new ADelegate(EventMethod)(a, EventArgs.Empty);
new BDelegate(EventMethod)(b, EventArgs.Empty);
new CDelegate(EventMethod)(c, EventArgs.Empty);
}

public static void EventMethod(A sender, EventArgs e)
{
Console.Out.WriteLine(sender.GetType().FullName);
}
}

Here I re-use a method with the base type. The first time you add an event handler, Visual Studio would of course pick the right type, so let's say this was the B event handler delegate. This method could now be used for both B and C objects. If you then wanted to use it for A as well, you'd need to change the method signature.

I know that for the generic EventArgs<T> type, you'd collide with generic contravariance, but that could be solved by just using EventArgs (which is the base type for the generic one). You'd still need to cast *when you wanted to share the method and wants access to specific things*.
Thursday, July 03, 2008 2:46:13 PM UTC
@LasseVK,
Yes, that works in the current version of C#. However, it didn't work in C# 1.0, when this guidance was created (event handlers had to match the delegate type exactly; delegate arguments were not covariant). I guess MS thinks that changing the guidance now would be confusing.
Thursday, July 03, 2008 8:12:05 PM UTC
Another thing to keep in mind is that reference equality can be a gotcha.

For example, I was writing some program the other day, and I found myself surprised that a reference equality check like the one Scott described failed. Why did it fail? Because of serialization.

At some point the instance I was using was serialized, and deserialized, so even though logically it "should be" the same object, it was not. When it was deserialized it got a new object reference, and my stored reference was NOT the same object reference as the one I got back.

I think this was in the context of using WCF and Microsoft's Peer to Peer services.. (Gosh my memory is crappy these days, this was less than a week ago, and I don't remember the context).

Anyhow, the point is, even if the code looks good, and seems to follow a sensible process, there might be external gotchas that mess you up. Always walk that kind of code and make sure it's doing what you want it to. Unless you're really sure that the reference will remain intact, then don't use reference comparisions.

I used a GUID on the types instead, and that worked well, as I could do a value comparison on the GUID...
Thursday, July 03, 2008 8:25:45 PM UTC
Excellent point about serialization.
Scott Hanselman
Friday, July 04, 2008 7:22:49 PM UTC
sth
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.