Abstract
I while ago I proposed a new DaST web dev pattern and its open-source ASP.NET implementation,
ASP.NET DaST Rendering Engine, as a solution having huge open-source dev potential
and outperforming all standard server-page frameworks such as ASP.NET Forms or MVC.
This is my 3rd article on DaST concept timed to the issue of RC version of DaST
library. Considering experience with my previous articles, this time I’ll not say
that DaST is smarter or better than any frameworks, especially MVC, in order to
not hurt feelings of some developers :) I’ll just explain how things work, state
only established facts, and I let you make your own conclusions which framework
is better for your web development work ;))
I know I promised DaST library production release 4 months ago, but unfortunately
I had to go abroad for a while, which delayed framework development quite a bit.
The version that has just been issued is Beta4 RC. This is a fully featured, tested,
and stable version of the framework, which you can download and start using in your
projects!
Initially, I was not going to write another article before production release, but
since I've made some minor syntax changes and one major architectural change to
how the scope tree is constructed, and there is no official documentation available
yet, I decided to explain my recent changes in more details, as some of you might
want to participate in DaST open-source project development. The version that has
just been issued is Beta RC and this article is to highlight the most important
differences of Beta4 compared to previous DaST framework versions. This article
will consist of 4 parts. First, I’ll quickly present a new TreeView DEMO application.
Next, I’ll describe all syntax and architectural changes made since the
previous version. Then we will delve into TreeView application
implementation to see all latest framework changes in action. And finally, I'll
shortly discuss my plans for next library releases.
All source code for this article can be downloaded from the SourceForge. Here are
some links:
Before You Read Anything
For the current article I’m assuming that you're already well familiar with DaST pattern and you at least read my previous article with the detailed tutorial for creating
VideoLibrary DEMO application using DaST. If not, then below I list some resources
that you should go through before reading any further:
If there are any questions after reading all this, dont hesitate to ask them on
the public DaST discussion forum on SourceForge. For convinience, I opened
this forum for anonimous access.
Also, throughout this article, I’ll use VideoLibrary DEMO as a sample for explanations
in some cases, so let’s agree that I’ll reference my previous article on VideoLibrary
application as “Article 2”.
Table of Contents
TreeView DEMO
This demo application is a prototype of a dynamic tree view displaying hierarchically
structured data with async on-demand loading of data branches. Hierarchical data
I’ve chosen for this application is a set of randomly nested folders and documents
(like old view of MSDN help). Another great example could be a discussion forum
thread with tree-like replies structure. Main purposes of TreeView DEMO application
are 1) to demonstrate simplicity and elegance of DaST pattern one more time, and
2) to show the importance of the latest design change consisting in on-demand construction
of the scope controller models. I deployed this application on my DaST blog, so
you can
run this demo LIVE right from the site. Typical UI of the application is shown
on Fig 1:
Fig. 1: TreeView DEMO application UI
So, the application simply displays a tree of folders and documents. Both folders
and documents can be nested in parent folders to reach the unlimited hierarchical
nesting structure. User is allowed to modify a tree by adding new or deleting existing
folders and documents, except that root folder cannot be deleted. If container folder
is deleted, all its child folders and documents are deleted too. Initially every
folder is displayed in “collapsed” state. When folder is clicked, its contents are
loaded by async postback action, and folder switches to “open” state. This UI prototype
is very primitive, but it could be used as a good starting point for implementing
more complex web applications requiring on-demand loading feature for hierarchical
data.
Nesting structure of folders and documents is defined by the XML. This physical
XML file has some initial folder/document structure in it and is loaded for each
session into memory staying there until current session ends. All changes made to
the tree are stored in this in-session XML object, but not in the physical file
meaning that all your changes are discarded whenever ASP.NET session renews. Fig
2 shows a part of this XML file which defines tree structure from Fig 1:
Fig. 2: XML data for TreeView application
Before we turn to implementation details of TreeView DEMO application, I'll go through
the differences between Beta4 RC and the last Beta2 version of the framework.
Beta4 RC Changes Overview
To understand everything I’ll be talking about in this section, you should read
my
Article 2 that uses another VideoLibrary application as demo for Beta2 framework
features. I’d like to make this section really short, so instead of posting code
snippets here, I’ll reference them in my previous article e.g. “Listing X in Article
2”. Also, VideoLibrary demo has been updated to run on the newest version of DaST
framework and its source code can be obtained from SourceForge in case you wish
to do deeper comparison with previous version of VideoLibrary from Article 2. So, let’s start from simple syntax changes.
Minor Syntax Changes
Below I'll just list several minor syntax changes that took place:
GetTemplate()
instead of SetTemplate(SetTemplateArgs template)
Now instead of assigning template content in SetTemplate(..)
function
(Listing 7 of Article 2), we just return template as string from GetTemplate()
function. Previous construct was used, because I thought we might need some other
settings modified in SetTemplateArgs
parameter, but seems like we don’t
need this feature anymore.
InitializeModel(..)
instead of SetupModel(..)
There is no hidden meaning here – I just like the second name better. So, implementing
controllers with new DaST library version, you simply use InitializeModel(..)
instead of SetupModel(..)
that was used before on Listing 7
of Article 2.
- Action handlers are passed generic object parameter.
Refer on Listing 7 of Article 2 again. Instead of having action parameters of type
ActionArgs
, we will now have parameters of generic object
type. If action is raised on the client side by DaST.Scopes.Action(..)
,
then this object will be a string
(whatever is passed in that client
call); otherwise, if action is raised from parent controller or invoked from child
controller (new functionality, read further sections for details), then action argument
could be any object
passed from parent or child controller. This construct
is simpler and more intuitive.
- More scope containers:
DIV
, SPAN
, and TD
As promised, I extended the list of valid scope containers. Now instead of DIV
,
you can also use SPAN
and TD
. The main criteria for the
valid container is that this DOM element must have an innerHTML
property
for the Ajax routine to be able to perform async refresh on it.
- Scope definition in template changed: “
dast:scope
” instead of “scope
”
Refer to Listing 6 of Article 2. We used “scope
” attributes on DIV
containers to tell the system that this is a scope. I think it’s a reasonable change
to have attribute namespace and use “dast:scope
” attributes instead.
Interim Scope Parameters
I decided to separate scope parameters into permanent parameters and interim parameters
that persist only during single rendering process and are not serialized. This is
a very reasonable change, because often we just want to pass values between controllers
within one postback and these values do not necessarily need to be serialized. Remember
that the more scope params are serialized, the more data is transferred between
async postbacks resulting in increased traffic.
ControlPath.Scope.Params
is a collection for permanent parameters,
the ones that you want to persist between async postbacks. Objects that you add
to this collection must be serializable to JSON (parameters are stored in JSON format).
Interim params are stored in ControlPath.Scope.InterimParams
collection.
Since values in this collection are not serialized, they can be just any generic
objects.
Major Change: Model Tree Built On Demand
One of the reasons for this article was actually this major change which I’d like
to emphasize, because it changes how we thought about rendering process before.
Recall
description of the rendering process in Article 2. In that version of DaST,
the model scope tree was build before the rendering process started. I.e. before
any data binding handlers in any controllers were called, the system had to construct
the complete model scope tree by invoking SetTemplate()
and
SetupModel()
functions for each controller one by one. And this actually works fine, but
only when we know the structure of model tree before the rendering process. But
what if I want to add some different child controller depending on the conditions
revealed only at rendering stage? Think about our TreeView application … When folder
is opened, its child items can be ether folders or documents. So, if folder has
N child items in it, the child scope has to be repeated N times to display these
N items and each item could be ether folder or document. Wnen model tree is built
from the beginning, after that we can only repeat scopes, but structure of the tree
does not ever change. In our case this causes the problem, because two repeated
child folders can have absolutely different child branches coming from them which
means different model trees. So, basically, before repeating each folder node, we
need to "adjust" its model subtree to be the one needed for this folder!
So it became clear that building the entire model tree at once is not the right
way to do it. The model tree should be built on per-controller basis every time
the rendering process hits the controller root scope. Look at Fig 5 of VideoLibrary
model tree in
Article 2. What’s the right way to build the model tree here? The rendering
process starts from NULL scope as before, but model tree must be empty at this time.
Then NULL scope is encountered, SetTemplate()
/SetupModel()
are called, and model tree gets built ONLY for NULL scope e.g. “AddButton” scope
is not known to the model tree at that time, because rendering traversal did not
come to the controller at “VideoItem” scope yet. Then traversal continues to all
child scopes, renders them, but whenever the system encounters some child controller
attached to the current scope e.g. “PlaylistPager”, the model tree for this
controller is built first, before invoking any data binding handlers i.e.
SetTemplate()/SetupModel
()
are called, and then data binding handlers are called. And so on – the
rendering process continues recursively until the entire tree is rendered. Next,
recall that if
“VideoItemRepeater” repeats
“VideoItem” N times, then
data binding handlers are called N times as well. Now this means that
SetTemplate()/SetupModel
()
are also called N times, on each rendering iteration before binding
handlers! And this is great, because now you can assign your child controllers directly
before each controller rendering iteration.
Another important thing is that you’re now allowed to use ControlPath.Scope.Params
inside the SetTemplate()
and SetupModel()
(in a new design
these are called GetTemplate()
and InitializeModel(..)
)
functions. This means that you can get all your serialized parameters and, depending
on them, set different controller template or add different child controllers for
current controller, right before this controller is rendered. Note that you can
use only permanent parameters here; interim params cannot be used within model initialization
functions. I’ll explain this below.
When page is loaded for the first time, the rendering process starts from the root
of the tree i.e. the entire tree is built. On the async postback there is absolutely
no need to rebuild the whole tree – we only need to build the branch to the action
scope. This means that before async action is processed, all
SetTemplate()/SetupModel
()
functions are called for the controllers on the action branch to restore
the model tree to the action scope. If further, during action processing, you invoke
action on the controller outside of the action branch, then model tree is rebuilt
for this controller on demand. Since model tree is restored, add parameters impacting
its structure, should be restored as well, because structure of the model tree lying
on action path should be exactly the same as after previous rendering, otherwise,
system will simply fail to find the target controller for current action. And this
is the reason for not allowing usage of interim params inside model setup functions
– we want the params to be restorable on the postback.
This explanation might sound a little bit complex, but it’s actually very simple
and intuitive once you get used to it. Look further at the TreeView application
implementation where this new on-demand model construction design becomes a key
to achieving the desired application functionality.
Methods Replaced By Actions
Recall scope methods mechanism
used in previous VideoLibrary described in Article 2. This mechanism is now completely gone (I never
liked it, to be honest :) Now, instead of calling methods on a controller, you will
simply invoke scope actions! So, to pass action to the parent controller you’ll
do ControlPath.Scope.RaiseAction("PlaylistUpdated", null)
as you did
before. But to tell the parent controller to execute some logic, you’ll just register
some scope action on it and invoke it manually e.g.
ControlPath.Fwd("PlaylistPager").Scope.InvokeAction("UpdatePagerValues",
new { ItemTotalCount = itemTotalCount })
to tell the pager controller to
recalculate its values. Take a look at the source code of the
new version of VideoLibrary application – it demonstrates this mechanism very
well.
You may say that actions do not completely replace methods, because they don’t return
values. Well, yes, but taking into account how clear and uniform your code becomes
having actions used everywhere, this limitation is reasonable and totally acceptable.
If you really need to return value from some action, you could set some ReturnValue
on the complex parameter passed to the action handler.
And we’re done with changes! Now let’s turn to the implementation details of TreeView
DEMO.
TreeView DEMO Implementation
This simple application demonstrates how the new on-demand model building design
works in the application with varying model tree, which was not easily implementable
with previous versions of DaST. And, as usual we start from planning a model scope
tree itself.
TreeView Model Tree
To achieve same UI as on Fig 1, I came up with the following model scope
tree structure displayed on Fig 3 below:
Fig. 3: Model scope tree for TreeView application
Recall from Article 2 that red nodes mean nodes
with controllers attached to them. There are 4 controllers used in this application:
RootController
, NodeSelector
, FolderNode
,
and DocumentNode
. RootController
is attached to “NULL”
root scope. NodeSelector
is attached to “Selector” scope. And
“Node” scope can have either FolderNode
or DocumentNode
attached to it. Since “Node” scope can have different controllers attached,
the model tree coming out of the “Node” scope can also be different: if
FolderNode
is attached, model tree goes on option (1), if
DocumentNode
is attached, model tree goes on option (2).
On Fig 3 I also specified actual templates, to which scopes in model tree
belong. These are depicted as green-colored areas with template file name on the
top. There are 4 templates corresponding to each of the controllers: RootTemplate.htm
used by RootController
, FolderTemplate.htm used by FolderNode
,
DocumentTemplate.htm used by DocumentNode
(this template is
depicted empty on the figure, because does not have any scopes, only HTML), and
<div dast:scope="Node"></div>
used by NodeSelector
(template defined as HTML text, because this controller returns a hardcoded string
instead of loading template from a file – refer to controller source code for more
details).
Finally, the idea how this is going to work is simple. NodeSelector
controller retrieves the corresponding XML element using XPath expression passed
to it. If element is a folder (see XML on Fig 2), then NodeSelector
assigns a FolderNode
controller to child “Node” scope. FolderTemplate.htm
corresponding to FolderNode
has 4 other scopes in it. “AddFolder”
scope has markup to input name of the new folder and submit it and this scope is
displayed only when user clicks one of “Add Folder” link buttons on the tree view.
Similar thing with “AddDocument” scope – this one is needed to add document
node to tree view. Most interesting scope here is “ChildItemRepeater” – it
takes care of displaying child items of the folder on a tree view. It has a “Selector”
child scope with NodeSelector
attached to it, meaning that “ChildItemRepeater”
renders NodeSelector
N times for each child item in the folder. Then
NodeSelector
is passed new XPath for every iteration, retrieves another
XML element, and so on – the process repeats recursively until all XML elements
are exhausted. By default, each folder is output in collapsed state i.e. none of
its child items are rendered, and the user has to click the folder to open it. In
case if XML element is a document, everything is much easier – DocumentNode
controller is assigned to “Node” and just renders some HTML to show document
on a tree view.
I’ll not go through any HTML template sources in this article – they are really
small and simple and you can just download the application
and take a look. Now, we are more interested in the back-end part of TreeView i.e.
4 scope controllers that I mentioned above. So let’s take a closer look at their
implementations with details and explanations.
Controller Implementations
Below I’m going to give complete code listings of all 4 controllers. One reason
for that is because I want you to think how you would implement the same TreeView
application in Forms or MVC and compare this to the DaST implementation. Note that
all TreeView implementation including async refresh logic is on the back-end and
there is 0 (zero) javascript in the templates – all programming you need is inside
your controller classes!
RootController controller
Let’s start from the RootController
whose code is shown below on Listing
1.
Listing 1: RootController source code
32: public class RootController : ScopeController
33: {
34: public override string ProvideTemplate()
35: {
36: return Utils.LoadTemplate("RootTemplate.htm");
37: }
38:
39: public override void InitializeModel(ControllerModelBuilder model)
40: {
41: model.Select("Selector").SetController(new NodeSelector());
42:
43: model.SetDataBind(new DataBindHandler(delegate()
44: {
45: ControlPath.Fwd("Selector").Scope.Params.Set("Path", "./*[1]");
46: }));
47: }
48: }
Everything is quite trivial here:
- Line 36 – specify the controller template.
- Line 41 – attach
NodeSelector
controller to the “Selector”
scope (the one that goes right after “NULL” root scope on Fig 3) - Lines 43-46 – controller root data binding. Note that I use inline delegate for
binding handler instead of full method references as I did before in Article 2. This form of syntax is just my personal preference
for this particular application, because it simpler and saves some space.
- Line 45 – inside root binding handler, we simply pass a “Path” parameter to the
NodeSelector
controller that we just added to the “Selector”
scope. This path contains a valid XPath expression that selects the target XML element
that NodeSelector
will process. In our case we pass the XPath of the
first child of the <treeview>
document element i.e. the root
<Folder name="Web Development">
element from Fig 1.
After rendering procedure passes through the RootController
, it encounters
NodeSelector
(that we just added to “Selector” scope) whose
source code goes next.
NodeSelector controller
Source code of NodeSelector
is below on Listing 2:
Listing 2: NodeSelector source code
32: public class NodeSelector : ScopeController
33: {
34: public override string ProvideTemplate()
35: {
36: return "<div dast:scope=\"Node\"></div>";
37: }
38:
39: public override void InitializeModel(ControllerModelBuilder model)
40: {
41: string path = ControlPath.Scope.Params.Get<string>("Path");
42: var xelem = DataLayer.GetHierarchyElement(path);
43:
44: if (xelem.Name == "Folder")
45: {
46: model.Select("Node").SetController(new FolderNode());
47: model.Select("Node").HandleAction("NodeDeleted", new ActionHandler(delegate(object arg)
48: {
49: ControlPath.Scope.RaiseAction("NeedRepeaterRefresh", null);
50: }));
51: }
52: else if (xelem.Name == "Document")
53: {
54: model.Select("Node").SetController(new DocumentNode());
55: model.Select("Node").HandleAction("NodeDeleted", new ActionHandler(delegate(object arg)
56: {
57: ControlPath.Scope.RaiseAction("NeedRepeaterRefresh", null);
58: }));
59: }
60: else throw new NotSupportedException("Not supported type of node in TreeView definition");
61:
62: ControlPath.Fwd("Node").Scope.Params.Set("Path", path);
63: }
64: }
The whole purpose of this controller is to attach the right child controller to
the “Node” scope to display ether folder or document. So, rendering procedure
encounters NodeSelector
and calls its ProvideTemplate()
and InitializeModel()
one after another to rebuild the model tree of
the current controller for the current rendering iteration.
- Line 36 – we return the controller template which basically consists of a single
“Node” child scope.
- Lines 41-42 – we retrieve the XPath expression (that we passed to the current
controller on line 45 of
RootController
) and get the actual XML element using this
expression. - Lines 44-51 – we test our element name if this is a folder and if this is, then
we attach
FolderNode
controller to “Node” scope, and handle
“NodeDeleted” action on this controller. Action handler simply passes “NeedRepeaterRefresh”
to the parent controller using RaiseAction(..)
call. - Lines 52-59 – same as before, but for
DocumentNode
controller. - Line 62 – we pass XPath further to the “Node” scope i.e. either
FolderNode
or DocumentNode
controller that we have just attached to it.
Note that we did not do any data binding here, because we simply do not need it.
Now, let’s assume that we have a folder element i.e. FolderNode
has
been added to the “Node” scope. In this case, the next controller hit by
rendering process is FolderNode
– the one that has the most complex
implementation.
FolderNode controller
Source code for this controller is below on Listing 3:
Listing 3: FolderNode source code
034: public class FolderNode : ScopeController
035: {
036: public override string ProvideTemplate()
037: {
038: return Utils.LoadTemplate("FolderTemplate.htm");
039: }
040:
041: public override void InitializeModel(ControllerModelBuilder model)
042: {
043: ControlPath.Scope.Params.Init("IsOpen", false);
044:
045: string path = ControlPath.Scope.Params.Get<string>("Path");
046: var folderElem = DataLayer.GetHierarchyElement(path);
047:
048:
049: model.Select("ChildItemRepeater", "Selector").SetController(new NodeSelector());
050:
051:
052: model.Select("ChildItemRepeater", "Selector").HandleAction("NeedRepeaterRefresh", new ActionHandler(delegate(object arg)
053: {
054: ControlPath.Fwd("ChildItemRepeater").Scope.Refresh();
055: }));
056:
057: model.HandleAction("ToggleOpen", new ActionHandler(delegate(object arg)
058: {
059: bool bOpen = !ControlPath.Scope.Params.Get<bool>("IsOpen");
060: ControlPath.Scope.Params.Set("IsOpen", bOpen);
061: ControlPath.Scope.Refresh();
062: }));
063:
064: model.HandleAction("ShowAddFolder", new ActionHandler(delegate(object args)
065: {
066: ControlPath.Fwd("AddFolder").Scope.Refresh();
067: ControlPath.Fwd("AddDocument").Scope.Refresh();
068: ControlPath.Fwd("AddDocument").Scope.RenderType = ScopeRenderType.Empty;
069: }));
070:
071: model.HandleAction("ShowAddDocument", new ActionHandler(delegate(object args)
072: {
073: ControlPath.Fwd("AddFolder").Scope.Refresh();
074: ControlPath.Fwd("AddDocument").Scope.Refresh();
075: ControlPath.Fwd("AddFolder").Scope.RenderType = ScopeRenderType.Empty;
076: }));
077:
078: model.HandleAction("SubmitFolder", new ActionHandler(delegate(object arg)
079: {
080: ControlPath.Fwd("AddFolder").Scope.Refresh();
081: ControlPath.Fwd("AddFolder").Scope.RenderType = ScopeRenderType.Empty;
082:
083: var folderName = Regex.Replace((string)arg, "[\\<\\>\r\n]", "", RegexOptions.Singleline);
084: if(!string.IsNullOrEmpty(folderName.Trim())) DataLayer.AddFolder(path, folderName.Trim());
085:
086: ControlPath.Fwd("ChildItemRepeater").Scope.Refresh();
087:
088: if (!ControlPath.Scope.Params.Get<bool>("IsOpen"))
089: {
090: ControlPath.Scope.Params.Set("IsOpen", true);
091: ControlPath.Scope.Refresh();
092: }
093: }));
094:
095: model.HandleAction("SubmitDocument", new ActionHandler(delegate(object arg)
096: {
097: ControlPath.Fwd("AddDocument").Scope.Refresh();
098: ControlPath.Fwd("AddDocument").Scope.RenderType = ScopeRenderType.Empty;
099: ControlPath.Scope.Params.Set("IsOpen", true);
100:
101: var documentName = Regex.Replace((string)arg, "[\\<\\>\r\n]", "", RegexOptions.Singleline);
102: if (!string.IsNullOrEmpty(documentName.Trim())) DataLayer.AddDocument(path, documentName.Trim());
103:
104: ControlPath.Fwd("ChildItemRepeater").Scope.Refresh();
105:
106: if (!ControlPath.Scope.Params.Get<bool>("IsOpen"))
107: {
108: ControlPath.Scope.Params.Set("IsOpen", true);
109: ControlPath.Scope.Refresh();
110: }
111: }));
112:
113: model.HandleAction("CancelFolder", new ActionHandler(delegate(object arg)
114: {
115: ControlPath.Fwd("AddFolder").Scope.Refresh();
116: ControlPath.Fwd("AddFolder").Scope.RenderType = ScopeRenderType.Empty;
117: }));
118:
119: model.HandleAction("CancelDocument", new ActionHandler(delegate(object arg)
120: {
121: ControlPath.Fwd("AddDocument").Scope.Refresh();
122: ControlPath.Fwd("AddDocument").Scope.RenderType = ScopeRenderType.Empty;
123: }));
124:
125: model.HandleAction("DeleteFolder", new ActionHandler(delegate(object arg)
126: {
127: DataLayer.DeleteHierarchyElement(path);
128: ControlPath.Scope.RaiseAction("NodeDeleted", null);
129: }));
130:
131:
132:
133: model.SetDataBind(new DataBindHandler(delegate()
134: {
135: CurrentPath.Scope.Binder.Replace("{FolderName}", folderElem.Attribute("name").Value);
136: CurrentPath.Scope.Binder.Replace("{XPath}", path);
137: CurrentPath.Scope.Binder.ApplyAreaConditional("not-root-node", folderElem.Parent.Name != "TreeView");
138:
139: bool bOpen = ControlPath.Scope.Params.Get<bool>("IsOpen");
140: CurrentPath.Scope.Binder.ApplyAreaConditional("state-open", bOpen);
141: CurrentPath.Scope.Binder.ApplyAreaConditional("state-collapsed", !bOpen);
142:
143:
144: ControlPath.Fwd("AddFolder").Scope.RenderType = ScopeRenderType.Empty;
145: ControlPath.Fwd("AddDocument").Scope.RenderType = ScopeRenderType.Empty;
146: }));
147:
148: model.Select("ChildItemRepeater").SetDataBind(new DataBindHandler(delegate()
149: {
150: bool bOpen = ControlPath.Scope.Params.Get<bool>("IsOpen");
151: CurrentPath.Scope.Binder.RestartRepeater();
152: if (bOpen)
153: {
154: for (int i = 0; i < folderElem.Elements().Count(); i++)
155: {
156: CurrentPath.Scope.Binder.Repeat();
157: string childPath = string.Format("{0}/*[{1}]", path, (i + 1).ToString());
158: CurrentPath.Fwd(i, "Selector").Scope.Params.Set("Path", childPath);
159: }
160: }
161: }));
162: }
163: }
This controller will actually draw the folder on the tree view. In addition to this,
we have to check folder state and if it is open, then output the child items of
the current folder which can be folders or documents. Again, for all action and
data binding handlers I use inline delegates, so the entire controller implementation
comes within InitializeModel(..)
function.
- Line 38 – return content of FolderTemplate.htm (scope skeleton of this
template is on Fig 1)
- Line 43 – use
Init(..)
to set “IsOpen” param if it is not set yet - Lines 45-46 – retrieve XPath expression (previously set on line 62 of
NodeSelector
)
and use it to get the folder element. Note that folderElem
variable
is accessible from inline delegate handlers defined further in
InitializeModel(..)
method. - Line 49 – attach
NodeSelector
controller to the “Selector”
scope coming out of the “ChildItemRepeater”. - Lines 52-55 – handle “NeedRepeaterRefresh” action raised by
NodeSelector
controller that we have just added. This action just tells the FolderNode
controller to refresh the “ChildItemRepeater” scope (line 54) i.e. redraw
folder child items. - Lines 57-62 – handle controller’s “ToggleOpen” action that occurs when user clicks
on the folder node in the tree. On this action we simply get folder state flag,
take logical NOT of it to toggle, and save the updated value. Then we refresh from
controller root to redraw the entire folder with all its contents.
- Lines 64-69 – handle controller’s “ShowAddFolder” action that occurs when user
clicks “Add Folder” link beside folder name. “AddFolder” scope (refer Fig
1) is initially invisible and we can display it simply by calling
Refresh(..)
on line 66. Recall from Article 2that
Refresh(..)
automatically sets scope RenderType
to
Normal
. In case if “AddDocument” scope is currently visible, we
need to hide it so we call Refresh(..)
and set its RenderType
to Empty
. - Lines 71-76 – same as lines 64-69, but for “ShowAddDocument” action.
- Lines 78-93 – handle controller’s “SubmitFolder” action that occurs after user
input the new folder name and click “Submit” link button. When folder is submitted,
we need to cleanup new folder input box (lines 80-81), get rid of bad characters
in name (line 83), and populate in-session XML with a new element (line 84) using
data layer. Next, we want to actually show our newly added folder, so we refresh
“ChildItemRepeater” scope (line 86) whose last child item will now be the
new folder that we just added. If folder is in collapsed state, then we need to open it automatically
when item is added – that’s what we have lines 88-92 for.
- Lines 95-111 – same as lines 73-93, but for “SubmitDocument” action.
- Lines 113-117 – handle controller’s “CancelFolder” action that occurs when user
clicks “Cancel” link button on the new folder input box. Here we simply hide the
new folder input.
- Lines 119-123 – same as lines 113-117, but for “CancelDocument” action.
- Lines 125-129 – handle controller’s “DeleteFolder” action that occurs when “Delete”
link button is clicked beside the folder name in the tree view. On this action,
we need to physically remove this folder element from in-session XML (line 127)
and let the parent folder know that something was deleted and it needs to redraw
its “ChildItemRepeater” scope (line 128).
- Lines 133-146 – we databind the controller’s root scope, providing placeholder
values (lines 135-136) and applying some area conditionals. “not-root-node” conditional
is needed to hide the “Delete” link button for root tree view node. “state-open”
and “state-collapsed” are needed to display correct folder image for different states.
On lines 144-145 we initially set both “AddFolder” and “AddDocument”
scopes to be invisible. We use
Empty
instead of None
,
because we still need scope container element to be output. - Lines 148-161 – finally, we databind the “ChildItemRepeater” scope. We
retrieve folder open flag and if open, then scope is repeated N times for each child
element in the current folder element. Repeating scopes, I construct the correct
XPath expression on each iteration and pass it to the “Selector” child scope
of “ChildItemRepeater”.
And we’re done with FolderNode
! Of course, to understand everything
completely, you’ll have to run and debug this application which I suggest you do.
The last controller is DocumentNode
which can be added by NodeSelector
in case of XML element is a document.
DocumentNode controller
Source code of DocumentNode
is below on Listing 4:
Listing 4: DocumentNode source code
32: public class DocumentNode : ScopeController
33: {
34: public override string ProvideTemplate()
35: {
36: return Utils.LoadTemplate("DocumentTemplate.htm");
37: }
38:
39: public override void InitializeModel(ControllerModelBuilder model)
40: {
41: string path = ControlPath.Scope.Params.Get<string>("Path");
42: var documentElem = DataLayer.GetHierarchyElement(path);
43:
44: model.HandleAction("DeleteDocument", new ActionHandler(delegate(object arg)
45: {
46: DataLayer.DeleteHierarchyElement(path);
47: ControlPath.Scope.RaiseAction("NodeDeleted", null);
48: }));
49:
50: model.SetDataBind(new DataBindHandler(delegate()
51: {
52: CurrentPath.Scope.Binder.Replace("{DocumentName}", documentElem.Attribute("name").Value);
53: CurrentPath.Scope.Binder.Replace("{XPath}", path);
54: }));
55: }
56: }
This one is much simpler than FolderNode
, because DocumentNode
cannot contain any items meaning that no actions or child scopes are needed. I’ll
not go into any details here, because source code is quite obvious.
And this is it! We’re completely done with our implementation of TreeView DEMO application.
Try to compare implementation above with what you would have in Forms – a complete
mess with user controls inside nested UpdatePanels
plus terrible design
in a code-behind class. And we always have to remember that on async postback DaST
executes rendering logic for impacted area ONLY, but Forms executes the entire page
handler from the beginning to the end which, besides killing hope for more or less
clear back-end code design, affects overall application performance.
MVC will do much better than Forms, but you’ll have to add a significant piece of
client script to get async updates to provide on-demand tree view branch loading.
You’ll have to create a separate partial view to serve branch loading ajax calls
and update the right spot on the tree view using javascript. Plus don’t forget about
Add New Folder/Document capabilities, deletion, and so on – the script might go
a bit complex in the end. And for sure it will not be simpler than just calling
Scope.Refresh(..)
in DaST implementation :)
Conclusion
I think that my next step on DaST framework development will be to create some quick
start dev reference and simple framework documentation. This will have to be done
before official DaST library release anyway and will take me some time (unless someone
will help me with this). Also, currently I’m busy with one commercial project on
DaST – internal administration console for one big e-commerce business. So, the
most likely date of DaST library release is around the end of November.
Also, this is the new open-source project and it needs your help! We need the open-source
developers and if you feel you can contribute to the project,
please join the team! The new DaST pattern is developing rapidly and so is
the framework. The design work is pretty much done, at least for now, but there
are still tons of improvements to the code and certain modules that can be made.
For example, params are serialized using Base64 over JSON which is ok for now, but
if we add simple compression, the serialized value could be reduced by 5 times.
Plus I’d really like VS integrated plugin to visualize the scope tree in the VS
designer. And so on – there is lots of dev work for this new library. Plus, after
ASP.NET version is rolled out, we will be starting open-source PHP DaST project,
so PHP developers are welcome to join DaST team too!
I’d really appreciate if you give this framework a try in your project and let the
team know about your experience, problems you’re having or framework bugs. Also,
we’re always open for suggestions and well-grounded criticism. You can always get
instant support or just share your opinion about the pattern on the
DaST Public Forum which is open for anonymous access.
And this is all for this article! I made it a bit more technical this time, but
I hope it was interesting anyway. DaST project home on the SourceForge is here. To get more information on DaST open-source project,
come to the project info site. The
summary of all latest DaST downloads is very useful and you can always find the
most updated versions there. Finally, to be well informed of the latest DaST related
events, please visit www.rgubarenko.net for all
framework news and updates or sign up for the project
mailing lists on SourceForge.
Software Architect with over 15 years in IT field. Started with deep math and C++ Computer Vision software. Currently in .NET and PHP web development. Creator of DaST pattern, open-source frameworks, and plugins. Interested in cutting Edge IT, open-source, Web 2.0, .NET, MVC, C++, Java, jQuery, Mobile tech, and extreme sports.