How to Really write a Server Control in ASP.Net
- November 13, 2013
Find Missing Dependency DLLs on Win 7
- March 5, 2012
Nullable TryParse
- May 26, 2011
Diff SQL Server Stored Procedures, November 15, 2010
Reporting Services Extranet Access, March 16, 2010
Case of the missing WaitCursor, January 7, 2009
Simple Submit Button Disable, December 9, 2009
An Efficient Memory Stream, September 29, 2009
Approach Plate Download - May 14, 2009
WPF Binding - Async Web Services - April 10, 2009
Developing the Blog
- April 4, 2009
|
|
Okay, it has been a long time since last blogging here. But, alas, that is not why you are here, so I will spare you the stories of
cross country trips, and Red Sox
championships...
So, I have a client who needed a clean interface for his website, and I worked
with a colleague on a new design. At first, we used a trial license of the
Componant Art Grid to do all our grids. But, sorry, that grid is too
awkward to work with. After searching about, I came across jqGrid. I have to admit, Tony Tomov did an incredible
job writing this library, and Oleg has done a huge job supporting it on Stack Overflow.
The guy is a machine.
Anyway. I began loving jqGrid the second I started working with it.
They thought of everything when writing it. The only thing missing was
that I had to write everything in Javascript, and I am working in ASP.Net, so
there was no way to consistently control the JS from the code behind. The
makers of jqGrid offer an ASP.Net control to do this, but, being a programmer myself,
I thought it was about time I bit off writing a Server Conrtol myself.
So, the first thing you do to write a Server Control in ASP.Net, is Google
ASP.Net Server Control. Good luck with that. The main articles are:
Developing Custom ASP.NET Server Controls and, when you really want to get
down and dirty, Custom Property State Management Example.
You basically start off leaning about how a control works, and how the postback
state works.
Now, if you don't know how ViewState works on an ASP.Net page, then you need to
brush up on that (I won't put a link here, as it will most likey go stale, but
just google "asp.net view state", and start reading). But the examples
given on the MS sites will lead you into writing a lot of code, and, eventually,
set you up for a costly rewrite.
For starters, let's look at their idea of property setters in a WebControl:
public virtual BookType BookType
{
get
{
object t = ViewState["BookType"];
return (t == null) ? BookType.NotDefined : (BookType)t;
}
set
{
ViewState["BookType"] = value;
}
}
Ok, you want to write this for every property in your class? Wouldn't it
be easier to write the following, instead?
public int?
Height
{
get { return
GetProp<int?>("Height");
}
set { SetProp(value,
"Height"); }
}
(And better yet, if you use .Net 4.5, and add a little code on the library, you can
just write:
public int?
Height
{
get { return
GetProp<int?>();
}
set { SetProp(value); }
}
but we'll get to that later).
The next problem with the examples has to do with tags nested within your server
control's tags. Take, for example, a common construct. Like a
collection of items within your control. You have the base control, which
is happy to collect viewstate for all the tag's attributes for you using the
above construct. But the examples for nested tags are nuts. Look at
how you are expected to save and load viewstate for contained (nested) tags:
protected override void LoadViewState(object savedState)
{
Pair p = savedState as Pair;
if (p != null)
{
base.LoadViewState(p.First);
((IStateManager)Author).LoadViewState(p.Second);
return;
}
base.LoadViewState(savedState);
}
protected override object SaveViewState()
{
object baseState = base.SaveViewState();
object thisState = null;
if (authorValue != null)
{
thisState = ((IStateManager)authorValue).SaveViewState();
}
if (thisState != null)
{
return new Pair(baseState, thisState);
}
else
{
return baseState;
}
}
This is from their "Custom Property State Management Example". Note how
they have invented this very specific case where they use a "Pair" class to
store the control class's view state, and then add a "Second" item to the class
to hold the nested class's state. This will work great if you only ever
have one nested class. But what about 6 classes. Or classes nesting
classes, and so on.
So, when I stared my jqGrid Server Control, I laid out the "schema" of the class
as follows:
<jq:Grid
ID="GLMasterGrid"
runat="server"
ClientIDMode="Static"
Caption="Current GL
Master Groups"
SortColumn="SeqNum"
PixelsFromBottom="440"
OnSelectRow="edit">
<Ajax WebMethod="GetGLMasterGrid"
EditMethod="AddGLMaster"
/>
<Editor Width="400" />
<Pager>
<ExcelExport
FileName="TripsWareGLMasterGroups"
/>
</Pager>
<Columns>
<jq:Column
PropertyName="CategoryGroupID"
Visible="false"
IsKey="true"
ExcelIgnore="true"
/>
<jq:Column
ID="DescName"
PropertyName="DescName"
Title="GL Master Group"
Width="225"
Search="true"
>
<Edit
Rules="required:true"
Options="size:30,
maxlength:150" />
</jq:Column>
<jq:Column
ID="GLGroupCode"
PropertyName="GLGroupCode"
Title="GL Master Group
Code" Width="225"
Search="true"
>
<Edit
Options="size:30,
maxlength:50" />
</jq:Column>
<jq:Column
ID="SeqNum"
PropertyName="SeqNum"
Title="Display Seq"
Width="90"
Align="Right">
<Edit
Rules="number:true,minvalue:0"
Options="maxlength:8,width:40"
/>
</jq:Column>
<jq:Column
ID="DeleteTemplate"
FormatterTemplate="DeleteTemplate"
Width="80"
Align="Center"
Sortable="false"
/>
</Columns>
</jq:Grid>
This is just an example, but demonstrates what I wanted to accomplish.
With the above tags, I was able to easily emit the jqGrid JavaScript that could
drive a grid for my client. There were properties to set the width of the
grid, and to drive the pager and editor, and specify the ajax calls to make.
Actually, the first cut at writing it went very fast, since I only handled
loading the control, and didn't deal with view state. That is kind of the
first dirtly little secret of server controls: If you are going to write an Ajax
control that doesn't care about postbacks, then, heck, you can just write a POCO
class and your're done. And that is what I had for my client for a few
months. When I did need to deal with changes to the underlying class on
postback, I just set the values again.
But to do it right, and really right, I was going to have to deal with the
nested viewstate problem.
So I broke the problem into 2 parts:
A typical WebControl comes raring to go with it's own ViewState object.
This object can serialize common types into the viewstate hidden field (int,
string, and even arrays). But don't try to store a complex type in there.
Like the MS example above, you need to develop some kind of trick to store and
load their viewstate. Instead of storing my nested tag and its atributes
in a Second parameter in a Pair class (or a third in a Triplet, and so on), I
just derived my nested tag's class from a new, IStateManager, class, called
StateManager. This class has the exact same behavior as a WebControl
class, in that it has a contained ViewState StateBag where it can store its
attributes, and deals with Save and Load (and IsTracking). Furthermore, I
introduced a new "object store" which sits next to the WebContol and
StateManager's ViewState which stores these nested objects, and can call on them
to Save and Load, since the nested objects stored in this dictionary are
IStateManager classes.
Next, I created a common class (StateManagerViewState) to store either
WebControl's ViewState, or a new StateBag for the the StateManager class, as
well as any nested objects, and whenever the control or one of its nested
controls sets a property, it trickles down to this contained class. A call
to SaveViewState or LoadViewState to either the WebControl or StateManager
object will correctly save and load all the properties as well as nested
objects. So, on postback, we get all the properties set for all the nested
objects
Finally, I implemented a StateManagerCollection<T> class, borrowing from the
MS StateManagedCollection class, but only collecting my new, StateManager
classes, and I piggy backed on the StateManager class to create a Dictionary
class, StateManagedDictionary<T>, which just stores values of type T in the
StateManager's base class's ViewState.
So, how does it all work? Well, all properties, whether they are in the
control or nested, simply have to be set by calling SetProp<T>(value,
"propName"). A nested object is set or retrieved with the
SetNestedProp<StateManager>(value, "propName") call, where the nested class must
be derived from StateManager.
public class
Grid :
StateManagerWebControl
{
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public
Pager Pager { get
{ return GetNestedProp<Pager>("Pager");
} }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public
Ajax Ajax { get
{ return GetNestedProp<Ajax>("Ajax");
} }
[Bindable(true), Category("Appearance"),
DefaultValue(""),
Localizable(true)]
public
new string Width
{
get { return
GetProp<string>("Width");
}
set { SetProp(value,
"Width"); }
}
[Bindable(true), Category("Appearance"),
DefaultValue(""),
Localizable(true)]
public
new string
Height
{
get { return
GetProp<string>("Height");
}
set { SetProp(value,
"Height"); }
}
and so on. Note how the class does not have to be cognizant of View State.
It is just getting and setting the property values by name. The base
classes take care of holding on to the view state, or nested classes and their
view states, and tracking and Save/Load. And, for example, the Ajax class
that is nested in this WebControl, above, is simply:
public class
Ajax :
StateManager
{
public
string WebMethod
{
get { return
GetProp<string>("WebMethod");
}
set { SetProp(value,
"WebMethod"); }
}
public
string EditMethod
{
get { return
GetProp<string>("EditMethod");
}
set { SetProp(value,
"EditMethod"); }
}
and so on. And, with the CallerMemberName attribute added to .Net 4.5,
we can just add a small change to the SetProp and GetProp functions to trim off
the "get_" and "set_" of the properties, and we can dispense with the
"propertyName" parameters altogether. At that point, everything would be
compiler checked!
public class
Ajax :
StateManager
{
public
string WebMethod
{
get { return
GetProp<string>();
}
set { SetProp(value); }
}
public
string EditMethod
{
get { return
GetProp<string>();
}
set { SetProp(value); }
}
Lastly, there is the code to support a collection:
public class
Column : StateManager
{
public
bool? IsKey
{
get { return
GetProp<bool?>("IsKey");
}
set { SetProp(value,
"IsKey"); }
}
public
bool? Visible
{
get { return
GetProp<bool?>("Visible");
}
set { SetProp(value,
"Visible"); }
}
etc
and the collection which holds it:
public class
JqColumnCollection :
StateManagerCollection<Column>
{
public
void RenderContents(string
tableID, HtmlTextWriter output)
{
output.WriteLine("var colModel = [");
output.Indent++;
string comma = " "; // first
line has no prepended comma
foreach (Column
column in this)
{
output.Write(comma);
column.RenderContents(tableID, output);
comma = ","; // now start prepending commas here on
out
}
output.Indent--;
output.WriteLine("];");
}|
and so on.
Here is the link to the code: StateManager.cs I doubt anyone will just
try and dump it in their code, but the essence is there. Feel free to
email me with questions, and I will try and answer you.
Hopefully this is a cleaner way to start your Server Control. I know now
that I have writen this, I will not shy away from writing them, as now I can
focus on the control and the HTML it emits, as opposed to worrying about whether
I have saved state correctly.
|