The module I am going to begin to develop is a news section
on the site, where it lists the latest news for that site. The ListView control
is a great control to use for this module for one simple reason: it gives you
complete control over the user interface layout. Having control over the layout
creates great flexibility in how the application works, and makes it really
easy to append JavaScript code. With a GridView, while it is nice to have this
control encapsulate the underlying UI because ASP.NET AJAX scripts need to know
what to work with under the scenes, a ListView is much easier to work with.
Take a look at the structure of a ListView below. This list
view renders both the read-only and the edit controls at the same time, but
hides the textboxes.
Listing 1: News ListView Definition
<asp:ListView ID="lvwNews" runat="server" ItemPlaceholderID="plcNewsList"
OnItemDataBound="lvwNews_ItemDataBound" InsertItemPosition="LastItem">
<LayoutTemplate>
<div id="NewsList">
<asp:PlaceHolder ID="plcNewsList" runat="server" />
</div>
</LayoutTemplate>
<ItemTemplate>
<asp:Label ID="lblNewsHeadline" runat="server"
Text='<%# Eval("Headline") %>' CssClass="NewsTitle" />
<asp:TextBox ID="txtNewsHeadline" runat="server"
Text='<%# Eval("Headline") %>' style="display:none" />
<br />
<asp:Label ID="lblNewsDescription" runat="server"
Text='<%# Eval("Description") %>' />
<asp:TextBox ID="txtNewsDescription" runat="server"
Text='<%# Eval("Description") %>' TextMode="MultiLine"
Rows="5" Columns="30" style="display:none" />
<br />
Posted at <asp:Label ID="lblNewsPostedDate" runat="server"
Text='<%# Eval("CreatedDate") %>' />
<asp:LinkButton ID="lnkEdit" runat="server"
OnClientClick="NewsEdit_Click();returnfalse;">Edit</asp:LinkButton>
</ItemTemplate>
<ItemSeparatorTemplate>
<br />
<hr />
<br />
</ItemSeparatorTemplate>
<InsertItemTemplate>
<span class="NewsTitle">Headline:</span>
<asp:TextBox ID="txtNewsHeadline" runat="server"
Text='<%# Eval("Headline") %>' />
<br />
<span class="NewsTitle">Description:</span>
<asp:TextBox ID="txtNewsDescription" runat="server"
Text='<%# Eval("Description") %>' TextMode="MultiLine"
Rows="5" Columns="30" />
<br />
<asp:LinkButton ID="lnkSave" runat="server" Text="Save"
OnClientClick="NewsInsert_Click();return false;" />
</InsertItemTemplate>
</asp:ListView>
This approach means that the initial setup of the table
renders on the server, and will be manipulated on the client using JavaScript
(newer items and edits sent via web services, while the client updates the UI).
Notice how the controls are laid out: the TextBox edit controls have their display
set to none, rather than setting Visible="false." Visible set to
false means the HTML element is not rendered in the browser, which is the wrong
response. Also, notice how buttons do not post back; this is prevented by using
the "return false;" statement in the OnClientClick. By default, the
__doPostback method is called after the OnClientClick code. The return false
statement prevents that code from being called. I would also recommend setting
the UseSubmitBehavior for any Button controls to false, so the button does not
render as a submit button (which ignores your code and posts back directly).
There should be careful planning in whatever approach is
taken. If the control is bound on the server, it means more content is passed
over the wire to the client (possibly in the megabytes). However, if the
client renders the UI, then only the data is passed over the wire, and the
markup is generated on the client. But this can be more complicated to setup.
However, whatever changes are made to the client are not
persisted in ViewState. If using the ListView approach, the ListView has to be
rebound on every page load if something changes; otherwise, it would not know
about the client-side code additions because it was not in ViewState. Using
the client only approach, the select web service call is called every postback,
unless some caching mechanism can be implemented.
Using the ListView route, the insert template and the edit
link will only be visible if the user has permissions. Because the news module
is in a user control, the user control exposes properties to turn this on or
off, and thus the server controls these capabilities.
To insert a new record, use the following code:
Listing 2: Inserting a Row of Data
function NewsInsert_Click() {
var body = $get("NewsInsertRow");
var headline = description = null;
for (var index = 0; index < body.childNodes.length; index++) {
var control = body.childNodes[index];
if (control.id != undefined && control.id != null
&& control.id.length > 0) {
if (control.id.endsWith("txtNewsHeadline"))
headline = control.value;
else if (control.id.endsWith("txtNewsDescription"))
description = control.value;
}
}
var newsItem = {
Headline: headline,
Description: description,
CreatedDate: new Date()
};
WebSiteStarterKit.Web.Services.NewsService.CreateNewsItem(newsItem,
function(results, context, method) { createNewsRow(results); },
function(results, context, method) { },
body);
}
There is some work in extracting the row information, a challenge
that I have been thinking about is how to make more efficient. The problem comes
with server-side templates. A server-side template field is not available via a
direct reference. What I mean is that any server control on a page can
reference it in JavaScript through the following code:
Listing 3: Referencing a server control on the
client
var label = $get("<%= lblLabel.ClientID %>");
At runtime, the client ID of the label is used. Client ID is
important because when using a master page, the ID of the label could be:
ctl100$ContentPlaceHolder$lblLabel, and thus, it is not good to hard-code this
value.
The challenge comes with the ListView. The ListView control
uses templates, meaning the reference to the control does not work because that
control may be repeated numerous times. To reference the txtNewsHeadline
control directly will not work because it resides in a template that may or may
not be bound, and referencing this way does not work.
I have thought a little about how to get around this. There
are a couple ways, which I will discuss later. One of those ways is the
approach I used above. The JavaScript code loops through the insert form,
looking for a control with the ID of the headline/description fields and
extracting their values. In JavaScript, controls (or their underlying elements)
can be accessed via the childNodes collection using the client-side HTML
reference. But the childNodes collection also contains literals, and thus
referencing an element via childNodes[0] may not contain the right result.
Furthermore, I do not like to change code, so my approach is
flexible in the sense that I do not have to rewrite code if I change the layout
of the page. For instance, if I insert a new header to make the insert section
look better, I would not have to change code when this changes childNode
collection indexes.
In the web service call at the end, the web service proxy
looks something like this:
<method>(<parameters separated by commas>, <success callback>, <failed callback>,
<context>);
The callback methods defined in this instance are inline
(functions do not have to be an explicit declaration, but can be passed inline
as an anonymous delegate works in C#), but the name of the method could also be
provided. This callback calls a method to create a new row in the user
interface, using the createNewsRow method.
Listing 4: Creating a new Row
function createNewsRow(newsItem) {
var body = $get("NewsList");
var insertRow = $get("NewsInsertRow");
var headlineLabel = document.createElement("SPAN");
headlineLabel.innerHTML = newsItem.Headline;
body.insertBefore(headlineLabel, insertRow);
var headlineBox = document.createElement("INPUT");
headlineBox.value = newsItem.Headline;
headlineBox.style.display = "none";
body.insertBefore(headlineBox, insertRow);
body.insertBefore(document.createElement("BR"), insertRow);
var descriptionLabel = document.createElement("SPAN");
descriptionLabel.innerHTML = newsItem.Description;
body.insertBefore(descriptionLabel, insertRow);
var descriptionBox = document.createElement("TEXTAREA");
descriptionBox.rows = 5;
descriptionBox.cols = 30;
descriptionBox.value = newsItem.Description;
descriptionBox.style.display = "none";
body.insertBefore(descriptionBox, insertRow);
body.insertBefore(document.createElement("BR"), insertRow);
var createdDateLabel = document.createElement("SPAN");
createdDateLabel.innerHTML = "Posted at: " +
newsItem.CreatedDate.format("MM/dd/yyyy hh:mm:ss") + " ";
body.insertBefore(createdDateLabel, insertRow);
var saveButton = document.createElement("A");
saveButton.innerHTML = "Edit";
saveButton.href = "javascript:NewsEdit_Click();return false;";
body.insertBefore(saveButton, insertRow);
body.insertBefore(document.createElement("BR"), insertRow);
}
This method does the work of appending a new row to the user
interface, working asynchronously to add the new entry to the database and to
the user interface. This method uses the Document Object Model (DOM) approach
to creating the user interface. It is a top down approach that creates the ion,
and then the user interface elements.
So what happens on the backend? A web service is what gets
called out of all of this, and the web service writes the data to an XML file that
resides in the web project, shown in Listing 5.
Listing 5: Creating a news item via web service
[
WebService(Namespace = "http://tempuri.org/"),
WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1),
System.ComponentModel.ToolboxItem(false),
ScriptService
]
public class NewsService : WebService
{
#region " Methods "
[WebMethod]
public NewsItem CreateNewsItem(object newsItem)
{
Dictionary<string, object> values = newsItem
as Dictionary<string, object>;
if (values == null)
throw new Exception(
"Cannot convert newsItem parameter to a collection");
NewsItem item = new NewsItem
{
Headline = (string)values["Headline"],
Description = (string)values["Description"],
CreatedDate = DateTime.Now
};
XmlDocument document = new XmlDocument();
document.Load(Server.MapPath("~/App_Data/News.xml"));
XmlElement element = document.CreateElement("Story");
document.DocumentElement.AppendChild(element);
element.AppendChild(document.CreateElement("Headline"));
element.AppendChild(document.CreateElement("Description"));
element.AppendChild(document.CreateElement("CreatedDate"));
element["Headline"].InnerText = item.Headline;
element["Description"].InnerText = item.Description;
element["CreatedDate"].InnerText = item.CreatedDate.ToString();
document.Save(Server.MapPath("~/App_Data/News.xml"));
return item;
}
#endregion
}
What gets passed up to the web service is a dictionary of
items (this is because what I passed up was in a name/value pair collection, a
typical class setup in JavaScript). This means that the values { Headline :
"Some Headline", "Description" : "Some
Description" } gets converted to a dictionary with a string key and an
object value. This is the typical approach; actually, if you use a JavaScriptConverter
object, you will see this approach widely used.
The reason for converting objects to string/object is
because all objects have to be serialized when passing them from web service to
client, or vice versa. Object references cannot be passed in directly; rather,
they are broken down into their primitive or serializable parts. This is the
same concept that happens when writing data to a database or serializing an
object to XML (an approach some Object Databases use). So the web service
simply has to read and write data from XML in order to store the data in this
solution, but the solution could easily use a database (I typically have used a
database to do this).