Sunday, January 21, 2007
#
I've used basic data binding in WinForms 2.0 a couple of times, but encountered something new to me this past week. We have a form that has a DateTimePicker control (including ShowCheckBox=true) and want to bind it to a Nullable object property. The requirement here is that the data value is optional for the user to enter (say, tracking a customer's Birthday) - so if there is no date entered, we want our data value to have null (and therefore, the DB has NULL).
We can't setup standard databinding on the DataBindings collection ...
dtBirthdayNullable.DataBindings.Clear();
dtBirthdayNullable.DataBindings.Add( "Value", person, "BirtdayNullable" );
...but this throws an exception - since the DateTimePicker.Value property can't be null.
From here I know that we need to control the data going into, and coming out of, the DateTimePicker control during the data binding process. This is where the Format and Parse events come in handy. The Format event lets you control how you handle data coming out of the object and going into the control property. The Parse event allows you to control how you deal with data coming out of the control before it gets set on your object property.
We setup the DataBindings using a Binding object, so we can hookup the event handlers:
Binding b = new Binding( "Value", person, "BdayNullable", true );
dtBirthdayNullable.DataBindings.Add( b );
b.Format += new ConvertEventHandler( dtBirthdayNullable_Format );
b.Parse += new ConvertEventHandler( dtBirthdayNullable_Parse );
Of special importance is the 4th parameter on the Binding constructor. This is to set formattingEnabled, so that we get the format event on the way in to the control.
Finally, the event handlers, and the trick in the Format method:
// OBJECT PROPERTY --> CONTROL VALUE
void dtBirthdayNullable_Format( object sender, ConvertEventArgs e )
{
// e.Value is the object value, we format it to be what we want to show up in the control
Binding b = sender as Binding;
if ( b != null )
{
DateTimePicker dtp = (b.Control as DateTimePicker);
if ( dtp != null )
{
if ( e.Value == null )
{
dtp.ShowCheckBox = true;
dtp.Checked = false;
// have to set e.Value to SOMETHING, since it's coming in as NULL
// if i set to DateTime.Today, and that's DIFFERENT than the control's current
// value, then it triggers a CHANGE to the value, which CHECKS the box (not ok)
// the trick - set e.Value to whatever value the control currently has.
// This does NOT cause a CHANGE, and the checkbox stays OFF.
e.Value = dtp.Value;
}
else
{
dtp.ShowCheckBox = true;
dtp.Checked = true;
// leave e.Value unchanged - it's not null, so the DTP is fine with it.
}
}
}
}
The key above is that we have to set the e.Value property to something (since the DateTimePicker control is not happy with a null value), but if we set it to something different than what it already is, the control interprets this just like the user changed the value, and therefore sets the checkmark (opposite of what we want/need).
So the trick is that we'll set e.Value to the what the DateTimePicker.Value already has.
Finish up with the Parse method, which will handle the data coming out of the control and into our object Property. Pretty straight forward:
// CONTROL VALUE --> OBJECT PROPERTY
void dtBirthdayNullable_Parse( object sender, ConvertEventArgs e )
{
// e.value is the formatted value coming from the control.
// we change it to be the value we want to stuff in the object.
Binding b = sender as Binding;
if ( b != null )
{
DateTimePicker dtp = (b.Control as DateTimePicker);
if ( dtp != null )
{
if ( dtp.Checked == false )
{
dtp.ShowCheckBox = true;
dtp.Checked = false;
e.Value = new Nullable();
}
else
{
DateTime val = Convert.ToDateTime( e.Value );
e.Value = new Nullable( val );
}
}
}
}
Thursday, January 05, 2006
#
One of our ASP.NET 1.1 applications is localized for 7 languages. I recently converted this app to ASP.NET 2.0, using Visual Studio 2005, and encountered some interesting challenges related to the language resources along the way.
1.1 Project Setup
We have the .aspx files along with their associated resx files (default resx, plus one for each language) in the root directory. When built, this results in the standard setup of default resources in the main code-behind assembly (bin\webSite.dll), and the language specific satellite resources in their respective directories (bin\de\webSite.resources.dll).
We have all pages in the application derive from a common BasePage, where we have some resource handling logic. Here in the base page, we create a ResourceManager for the current page, then walk all the controls on the page, and find the appropriate resource text for each control (if provided). The highlights of this code is shown below. Pretty standard / straight forward, right?
ResourceManager rm = new ResourceManager( this.GetType().BaseType.FullName,
System.Reflection.Assembly.GetExecutingAssembly() );
The Conversion
When I opened up this project in VS 2005, I was presented with the project conversion wizard as expected, but found it interesting what the conversion process did with my resource files. Most of the resource files were moved to the newly created "App_GlobalResources" directory. But...it LEFT the base resource file for each page (i.e. Home.aspx.resx) in the main directory where the aspx pages are.
The Build
When this new 2.0 project layout is built, the base resx files in the main aspx directory are completely ignored, and the "App_GlobalResources" directory is built into it's own assembly. This causes problems with the above BasePage code, trying to create a ResourceManager for the currently executing page, and its associated resources. The ResourceManager will find no resources for the controls on the page, since they are in the separate App_GlobalResources assembly.
The Problem
Obviously, the start of the problem is that the 2nd parameter to ResourceManager constructor above (in red) is incorrect, since the currently executing assembly in ASP.NET 2.0 is the "web" assembly, which is different than the assemblies that hold the resources (global and local).
I know ASP.NET 2.0 comes with new support for resources. However, the programming model is quite a bit different. After adding resources to App_GlobalResources, you can refer to them in your code by using the Resources class which is dynamically generated as a wrapper for the contents of App_GlobalResources. Something like:
// use this in code-behind to set label text based on App_GlobalResources
lblFoo.Text = Resources.SomePageName.Foo;
// use this in aspx page markup to set label text based on App_LocalResources
The problem is, I didn't want to retrofit all of our pages to use either of the above syntax - rather, I wanted to come up with a way to keep the 1.1 code flow - allowing the BasePage to create a proper ResourceManager that would be pointing to those resources from App_GlobalResources. But -
how do you create a ResourceManager at runtime pointing to the App_GlobalResources assembly when this assembly filename changes with every build?
Some Workarounds / Solutions
After digging around, I came up with a few solutions, some better than others, and the ultimate one, using an (as far as I can see) undocumented feature.
The 2.0 techniques for retrieving resources all seem to bypass the ResourceManager class all-together. At least the calling code doesn't get the ResourceManager to use.
// for the Default page, whose class name is _default
// #1 - use built in dynamically generated Resources class. Provides
// strongly typed names for each resource
lblFoo.Text = Resources._default.foo;
// #2 - use TemplateControl.GetGlobalResourceObject()
lblFoo.Text = GetGlobalResourceObject("_default", "foo").ToString();
Neither of these help in my quest to keep my 1.1 code working as is. I need a ResourceManager object that refers to the resources for the current page for the rest of the control-walking code to work.
The trick is, to create the proper ResourceManager, you need 2 things: the type name (easy, this is the class name of the code-behind class for the currently running page), and the assembly where the resources are. We don't know the name of the App_GlobalResources assembly since it's different each time we compile (ex: App_GlobalResources.fsfa3s_h.dll).
How should we find the App_GlobalResources assembly?
Here are the thoughts I came up with along the way, finally tripping over the last solution, based on a clue mentioned in passing in a ASP.NET 2.0 webcast.
// #1 - HUGE HACK! Since the page/code-behind executing assembly
// references the App_GlobalResources assembly,
// walk the references and find it.
{
Assembly asmGlobal = null;
Assembly asmCode = Assembly.GetExecutingAssembly();
foreach (AssemblyName name in asmCode.GetReferencedAssemblies())
{
// munged name will at least have App_GlobalResources in it's name
if (name.Name.IndexOf("App_GlobalResources") != -1)
{
asmGlobal = Assembly.Load(name);
break;
}
}
if (asmGlobal != null)
{
string resName = String.Format( "Resources.{0}",
this.GetType().BaseType.Name );
ResourceManager rm = new ResourceManager(resName, asmGlobal);
lblFoo.Text = rm.GetString ("foo" );
}
}
// #2 - Slightly better. If you know at least one resource name in the
// App_GlobalResources assembly, at compile time, you can use it to give
// you a type, which gives you the containing assembly.
// At least we don't have to walk a list of references...
{
// can use this same name for all calls, just using to
// get the type/assembly
Type t = typeof(Resources._default);
Assembly asm = t.Assembly;
string resName = String.Format("Resources.{0}",
this.GetType().BaseType.Name);
ResourceManager rm = new ResourceManager(resName, asm);
lblFoo.Text = rm.GetString( "foo" );
}
// #3 - Shortcut for #2 above. Just use the Resources dynamic
// wrapper class, and it has a
// ResourceManager property.
{
ResourceManager rm = Resources._default.ResourceManager;
lblFoo.Text = rm.GetString( "foo" );
}
// #4 - Uses the undocumented support for Assembly.Load with the special
// name "App_GlobalResources". Turns out, the ASP.NET 2.0 runtime wires
// up the AppDomain.AssemblyResolve event, which
// fires when an assembly fails to load based on the provided name. See
// below, if you ask to load the "App_GlobalResources" assembly, which
// doesn't exist (at least by that name), the AssemblyResolve() event
// handler finds the correct global resources assembly, and loads it.
// Someday in my spare time, I'll try to find the code where this happens.
{
// This will immediately find the desired assembly,
// regardless of the name.
Assembly asmGlobal = Assembly.Load( "App_GlobalResources" );
// just need short name, not full strong name
string resName = String.Format( "Resources.{0}",
this.GetType().BaseType.Name );
ResourceManager rm = new ResourceManager( resName,
<Assembly.Load("App_GlobalResources") );
lblFoo.Text = rm.GetString( "foo" );
}
The Bottom Line
As you can see above, there are many ways to find the App_GlobalResources assembly at runtime if you need to. If you're using the new ASP.NET 2.0 support for resources, you most likely don't care what/where the assembly is, but to support legacy code, you might just have to find it.
I like #3 above, a quick 2 lines of code to create the proper ResourceManager object. Although it's good to know you can directly find the App_GlobalResources assembly with the Assembly.Load() special handling.
Special note: you can also find the App_Code assembly with
Assembly.Load( "__code" );
Monday, October 10, 2005
#
I am always looking for new features in the Visual Studio IDE that help streamline the everyday coding and debugging tasks that we do. At PDC, I attended a good session on productivity enhancements in the Visual Studio 2005 IDE. (Thanks to Anson Horton for the content.)
Here are just a few of the new features that I'm sure I'll be using.
DebuggerStepThrough
When you're stepping through lines of code in the debugger, many times you want to step INTO a method call. However, if it has some parameter arguments that are themselves method or constructor calls, you end up stepping into those first. Visual C++ 6 had a feature that I really liked, and miss now - you could right click on the function call, and "Step Into ". VS.NET IDEs do not have that, so I've gotten good in the past few years at F11, Shift+F11, F11, Shift+F11, ...
There is now a solution: The DebuggerStepThrough attribute.
You can apply it to any method or constructor.
using System.Diagnostics;
[DebuggerStepThrough()]
public Person( string name, string phone )
...
// Now the debugger will step straight into the CallPerson method
p.CallPerson(new Person("dan", "5551212"));
Unfortunately, I couldn't get this working on a property. Compiler error says I can only apply this attribute to class, struct, constructor, or method. I wonder why they didn't extend this to properties?
Debugger Tool Tips
In the last few versions of the IDE, the debugger has displayed tool tips when you mouse-over a variable. The bummer is, for objects, the tooltip information is usually not that helpful - it just tells you the type of the object.
"ConsoleApp1.Person" -- not a lot of info here.
Using another attribute, DebuggerDisplay, you can control what the debugger will show in the tool tips for a class
[DebuggerDisplay( "Name={name}, Phone={phone}" )]
Now, in the debugger, you can probably guess what you'll see:
Name="Dan", Phone="5551212"
Simple things like this prevent you from having to right click, quickwatch, and drill down into the object to see relevant data. This formatting is also used for the top level display in the autos and watch windows.
In previous versions of the IDE, you could control this display in the autoexp.dat (c++), or autoexp.cs (c#) files. You can still do this for classes that you don't have source code for.
Find this file in \Common7\Packages\Debugger\Visualizers\Original
DebuggerBrowsable
Finally, when you expand the debugger display for a class with properties, you will usually see the properties, and then the private fields, which repeat the same data from the properties.
Name "Dan"
name "Dan"
Phone "5551212"
phone "5551212"
Use the DebuggerBrowsable attribute to control what data is displayed when drilling down into a class.
[DebuggerBrowsable( DebuggerBrowsableState.Never )]
public string Name
...
Now you'll only see the field:
name "Dan"
Visualizers Getting in the Way?
A very cool feature of VS 2005 is visualizers. You'll probably see them in your first debug session in the new IDE. They are the expanding tool tip windows that show hierarchies of classes, structs, data that is in the variable you're watching.
Well, the more you expand, the more code you obscure behind the "tree" windows.
Want to quickly check something in the code that's behind all those windows?
Hold down CTRL !! The popup windows become transparent -- very cool.