Monday, August 4, 2008

App_Code vs. ascx: Differences you should know

App_Code vs. ascx: Differences you should know: "App_Code vs. ascx: Differences you should know"
There's an important factor you should consider when deciding whether to implement your control as an .ascx UserControl or as a Control subclass in App_Code.
Will you ever need to pass a relative path to the control, through a method or attribute? Does your control function as a container for markup? .ascx UserControls believe that all relative paths are relative to them. I've experimented with a lot of different things, and I'm convinced there is no elegant workaround. While it is possible to set the value of AppRelativeTemplateSourceDirectory, it's not possible to reliably figure out what the value should be. If your user controls and pages are in the same directory, you may not discover this problem until you reorganize your app files!
The instinctive approach is to reach for the .Parent property, but that doesn't work in a some common situations. In particular, this approach won't work directly inside controls, since they incorrectly report themselves as children of the master page instead of the content page. Correcting for this becomes difficult if you plant to support the use of the controls within the master pages themselves, or support nested master pages. I use content pages exclusively, and I like to keep things simple - so at least 95% of my server controls are right inside the root tags. Ouch.
If your control is going to contain other arbitrary controls, markup, or accept paths, you definitely need to go with the App_Code approach. Going with an .ascx file will make all child controls (even if built correctly!) rebase paths relative to your user control, not the file containing the markup.
In case you're curious, the TemplateControl class (which UserControl, Page, and MasterPage inherit from), overrides the TemplateControl property from the Control class.
view plaincopy to clipboardprint?
internal override TemplateControl GetTemplateControl()
{
return this;
} internal override TemplateControl GetTemplateControl()
{
return this;
}
And when your .ascx file is translated into source code for compilation, the following line is inserted into the Page_Load event handler:
view plaincopy to clipboardprint?
((System.Web.UI.UserControl)(this)).AppRelativeVirtualPath =
"~/cms/events/EventTable.ascx"; //Or whatever the physical location is ((System.Web.UI.UserControl)(this)).AppRelativeVirtualPath =
"~/cms/events/EventTable.ascx"; //Or whatever the physical location is
Application-relative paths can still be used, but are very fragile and sensitive to movement in the parent folder structure. Most paths (like images, css, slideshows, and videos) are best expressed in relative form. As a rule, use relative paths for related files.
I discovered this problem after I already had a series of controls written in .ascx form, so I e-mailed Scott Guthrie about the dilemma. He passed my e-mail on to Matt Gibbs, who was very helpful as always.
The behavior of .ascx files is actually by design - they aren't designed to have a resident filepath-agnostic mode. He also suggested placing an control right inside the tags to make .Parent work in that situation.
Since I already had several hundred content pages, I ended up rewriting most of my controls in App_Code instead. For a few controls I really wanted to keep in declarative markup, I used a hack to automatically change AppRelativeTemplateSourceDirectory. Even though I can't fix the TemplateControl structure, I can change the AppRelativeTemplateSourceDirectory to mimic the proper behavior.
view plaincopy to clipboardprint?
///
/// Set this to true if you externally set the AppRelativeTemplateSourceDirectory.
/// Otherwise, the externally set value will be ignored.
///

public bool LocationOverriden
{
get
{
return _LocationOverriden;
}
set
{
if (value)
if (!_LocationOverriden) _LocationOverriden = true;
}
}
protected override void OnLoad(EventArgs e)
{
if (!LocationOverriden) ImpersonateParentLocation();
base.OnLoad(e);
}
///
/// Retrieves the correct parent template control of the current template control
/// Doesn't support use within .master files, only content pages. Support for master pages could be added with more granular type checks.
///

public Control ParentTemplateControl
{
get
{
if (this.Parent != null)
if (this.Page != null)
{
if (this.Parent.TemplateControl == this.Page.Master)
return this.Page;
else
return this.Parent.TemplateControl;
}
return null;
}
}

///
/// Changes AppRelativeTemplateSourceDirectory to match the parent file, if it hasn't already been overriden.
/// Paths
///

public void ImpersonateParentLocation()
{
if (ParentTemplateControl != null)
this.AppRelativeTemplateSourceDirectory = ParentTemplateControl.AppRelativeTemplateSourceDirectory;
} ///
/// Set this to true if you externally set the AppRelativeTemplateSourceDirectory.
/// Otherwise, the externally set value will be ignored.
///
public bool LocationOverriden
{
get
{
return _LocationOverriden;
}
set
{
if (value)
if (!_LocationOverriden) _LocationOverriden = true;
}
}
protected override void OnLoad(EventArgs e)
{
if (!LocationOverriden) ImpersonateParentLocation();
base.OnLoad(e);
}
///
/// Retrieves the correct parent template control of the current template control
/// Doesn't support use within .master files, only content pages. Support for master pages could be added with more granular type checks.
///
public Control ParentTemplateControl
{
get
{
if (this.Parent != null)
if (this.Page != null)
{
if (this.Parent.TemplateControl == this.Page.Master)
return this.Page;
else
return this.Parent.TemplateControl;
}
return null;
}
}
///
/// Changes AppRelativeTemplateSourceDirectory to match the parent file, if it hasn't already been overriden.
/// Paths
///
public void ImpersonateParentLocation()
{
if (ParentTemplateControl != null)
this.AppRelativeTemplateSourceDirectory = ParentTemplateControl.AppRelativeTemplateSourceDirectory;
}
You are welcome to read (and vote for!) the Microsoft Connect issue

No comments: