In my travels with an ASP.NET Mobile Device application, I ran into a need to display a status panel as the result of a repeated method call on a timed basis from the page. Under normal ASP.NET page scenarios this is easy: you can either use Javascript (such as with an Anthem.net timer control) or you can create a META Refresh tag inside your Page's HEAD tag, set the HEAD to runat=server, and use an HtmlMeta control to set your HtmlMeta tag Content attribute for the refresh time and / or redirect URL. In other words, you can either display a changing status message or you can display a message and then automatically redirect to another page after 5 seconds.
Unfortunately, you cannot do either in ASP.NET Mobile forms. Devices are too dumb to handle client script, and the HtmlMeta tag won't render in a Mobile Page.
So, I did a little searching and came up with a nice arrangement that comes right out of the ASP.NET 1.1 Quickstarts. The code sample did almost everything I needed, so I refactored it out into a separate control library and made it easier to use. Here is the control code, virtually unchanged from the example:
using System;
using System.Collections;
using System.Web.UI.MobileControls;
using System.Web.UI.MobileControls.Adapters;
namespace Acme
{
public class TimerFormCS : Form
{
protected static readonly Object EventTimer = new Object();
// ================================================================
//
// Delay property
//
// Defines, in seconds, how long to delay before executing action.
// Defaults to ten seconds.
//
// ================================================================
public int Delay
{
get
{
Object o = ViewState["Delay"];
return o != null ? (int)o : 10;
}
set
{
ViewState["Delay"] = value;
}
}
// ================================================================
//
// AutoNavigateUrl property
//
// If defined, renders markup to make the browser automatically
// navigate to the given URL after the specified time.
//
// ================================================================
public String AutoNavigateUrl
{
get
{
String s = (String)ViewState["AutoNavigateUrl"];
return s != null ? s : String.Empty;
}
set
{
ViewState["AutoNavigateUrl"] = value;
}
}
// ================================================================
//
// Timer event
//
// This event is raised when the timer elapses, and the browser
// posts back to the server. If AutoNavigateUrl is defined, no
// postback happens.
//
// ================================================================
public event EventHandler Timer
{
add
{
Events.AddHandler(EventTimer, value);
}
remove
{
Events.RemoveHandler(EventTimer, value);
}
}
// ================================================================
//
// OnTimer method
//
// This protected method allows inheriting classes to internally
// handle a post back following the specified delay. The default
// implementation raises the Timer event.
//
// ================================================================
protected virtual void OnTimer(EventArgs e)
{
EventHandler onTimer = (EventHandler)Events[EventTimer];
if (onTimer != null)
{
onTimer(this, e);
}
}
// ================================================================
//
// RaiseTimer method
//
// Called by the adapters to raise the timer event.
//
// ================================================================
public void RaiseTimer()
{
OnTimer(new EventArgs());
}
}
// ====================================================================
//
// WmlTimerFormAdapter Class
//
// The WmlTimerFormAdapter class renders a TimerForm control on
// WML devices. The timer functionality is rendered using an
// <onevent type="timer"> construct. All other behavior
// is inherited from WmlFormAdapter.
//
// ====================================================================
public class WmlTimerFormAdapterCS : WmlFormAdapter
{
private const String TimerEventArgument = "$timer";
protected new TimerFormCS Control
{
get
{
return (TimerFormCS)base.Control;
}
}
// ================================================================
//
// RenderExtraCardElements method
//
// By overriding this method, the adapter can render additional
// content immediately after the <card> tag in the WML output.
//
// ================================================================
protected override void RenderExtraCardElements(WmlMobileTextWriter writer)
{
String autoNavigateUrl = Control.AutoNavigateUrl;
// A URL to another form on the same page will also cause a postback.
bool renderAsPostBack = autoNavigateUrl.Length == 0 ||
DeterminePostBack(autoNavigateUrl) != null;
writer.WriteBeginTag("onevent");
writer.WriteAttribute("type", "ontimer");
writer.Write(">");
if (renderAsPostBack)
{
writer.RenderGoAction(Control.UniqueID,
TimerEventArgument,
WmlPostFieldType.Normal,
false);
}
else
{
// Resolve the URL relative to the page.
autoNavigateUrl = Control.ResolveUrl(autoNavigateUrl);
writer.WriteBeginTag("go");
writer.Write(" href=\"");
writer.WriteEncodedUrl(autoNavigateUrl);
writer.Write("\">");
}
writer.WriteEndTag("onevent");
writer.WriteLine();
writer.WriteBeginTag("timer");
// WML timer lengths are in 10th of seconds.
writer.WriteAttribute("value", (Control.Delay * 10).ToString());
writer.WriteLine("/>");
}
// ================================================================
//
// HandlePostBackEvent method
//
// By overriding this method, the adapter can handle the postback
// generated from the timer.
//
// ================================================================
public override bool HandlePostBackEvent(String eventArgument)
{
if (eventArgument == TimerEventArgument)
{
String autoNavigateUrl = Control.AutoNavigateUrl;
if (autoNavigateUrl.Length > 0)
{
if (autoNavigateUrl.Length > 1 && autoNavigateUrl[0] == '#')
{
Page.ActiveForm = Control.ResolveFormReference(autoNavigateUrl.Substring(1));
}
}
else
{
Control.RaiseTimer();
}
return true;
}
else
{
// Let the base adapter class handle any events.
return base.HandlePostBackEvent(eventArgument);
}
}
}
// ====================================================================
//
// HtmlTimerFormHelper Class
//
// The HtmlTimerFormHelper class is a helper class used by both
// HtmlTimerFormAdapter and ChtmlTimerFormAdapter. Although both
// these classes require identical functionality, they have different
// base classes, making a helper class useful.
//
// On HTML devices, the timer functionality is rendered using a
// <meta http-equiv="refresh"> construct. All other behavior
// is inherited from the base form adapter.
//
// ====================================================================
class HtmlTimerFormHelperCS
{
private const String TimerEventArgument = "$timer";
// ================================================================
//
// RenderTimerMetaTag method
//
// Renders the metatag for the timer behavior.
//
// ================================================================
public static void RenderTimerMetaTag(TimerFormCS form, HtmlMobileTextWriter writer)
{
String autoNavigateUrl = form.AutoNavigateUrl;
// A URL to another form on the same page will also cause a postback.
bool renderAsPostBack = autoNavigateUrl.Length == 0 ||
autoNavigateUrl[0] == '#';
writer.WriteBeginTag("meta");
writer.WriteAttribute("http-equiv", "refresh");
writer.Write(" content=\"");
writer.Write(form.Delay.ToString());
writer.Write(";url=");
if (renderAsPostBack)
{
HtmlPageAdapter pageAdapter = (HtmlPageAdapter)form.MobilePage.Adapter;
pageAdapter.RenderUrlPostBackEvent(writer, form.UniqueID, TimerEventArgument);
}
else
{
writer.WriteEncodedUrl(autoNavigateUrl);
}
writer.WriteLine("\">");
}
// ================================================================
//
// HandlePostBackEvent method
//
// Handles a timer postback event.
//
// ================================================================
public static bool HandlePostBackEvent(TimerFormCS form, String eventArgument)
{
if (eventArgument == TimerEventArgument)
{
String autoNavigateUrl = form.AutoNavigateUrl;
if (autoNavigateUrl.Length > 0)
{
if (autoNavigateUrl.Length > 1 && autoNavigateUrl[0] == '#')
{
form.MobilePage.ActiveForm = form.ResolveFormReference(autoNavigateUrl.Substring(1));
}
}
else
{
form.RaiseTimer();
}
return true;
}
else
{
return false;
}
}
}
// ====================================================================
//
// HtmlTimerFormAdapter Class
//
// The HtmlTimerFormAdapter class renders a TimerForm control on
// HTML devices. The timer functionality is rendered using a
// <meta http-equiv="refresh"> construct. All other behavior
// is inherited from HtmlFormAdapter.
//
// ====================================================================
public class HtmlTimerFormAdapterCS : HtmlFormAdapter
{
protected new TimerFormCS Control
{
get
{
return (TimerFormCS)base.Control;
}
}
// ================================================================
//
// RenderExtraHeadElements method
//
// By overriding this method, the adapter can render additional
// content inside the <head> tag of the rendered page. The adapter
// must call the base class implementation.
//
// ================================================================
protected override bool RenderExtraHeadElements(HtmlMobileTextWriter writer)
{
base.RenderExtraHeadElements(writer);
// The method is called twice - once with the writer set to null,
// to determine if there is anything to be written; and once with
// a valid writer.
if (writer != null)
{
HtmlTimerFormHelperCS.RenderTimerMetaTag(Control, writer);
}
return true;
}
// ================================================================
//
// HandlePostBackEvent method
//
// By overriding this method, the adapter can handle the postback
// generated from the timer.
//
// ================================================================
public override bool HandlePostBackEvent(String eventArgument)
{
if (HtmlTimerFormHelperCS.HandlePostBackEvent(Control, eventArgument))
{
return true;
}
else
{
// Let the base adapter class handle any events.
return base.HandlePostBackEvent(eventArgument);
}
}
}
// ====================================================================
//
// ChtmlTimerFormAdapter Class
//
// The ChtmlTimerFormAdapter class renders a TimerForm control on
// scriptless HTML devices. The timer functionality is rendered using
// a <meta http-equiv="refresh"> construct. All other behavior
// is inherited from ChtmlFormAdapter.
//
// ====================================================================
public class ChtmlTimerFormAdapterCS : ChtmlFormAdapter
{
protected new TimerFormCS Control
{
get
{
return (TimerFormCS)base.Control;
}
}
// ================================================================
//
// RenderExtraHeadElements method
//
// By overriding this method, the adapter can render additional
// content inside the <head> tag of the rendered page. The adapter
// must call the base class implementation.
//
// ================================================================
protected override bool RenderExtraHeadElements(HtmlMobileTextWriter writer)
{
base.RenderExtraHeadElements(writer);
// The method is called twice - once with the writer set to null,
// to determine if there is anything to be written; and once with
// a valid writer.
if (writer != null)
{
HtmlTimerFormHelperCS.RenderTimerMetaTag(Control, writer);
}
return true;
}
// ================================================================
//
// HandlePostBackEvent method
//
// By overriding this method, the adapter can handle the postback
// generated from the timer.
//
// ================================================================
public override bool HandlePostBackEvent(String eventArgument)
{
if (HtmlTimerFormHelperCS.HandlePostBackEvent(Control, eventArgument))
{
return true;
}
else
{
// Let the base adapter class handle any events.
return base.HandlePostBackEvent(eventArgument);
}
}
}
}By revising your Page to derive its form from this control, instead of the regular Mobile Form, you get all the necessary event "stuff" to have a self-refreshing Mobile Form, along with some very nice DeviceAdapter device-specific code as well. Creating a self-refreshing Form for ASP.NET Mobile is not as easy as it might seem at first blush; to do it right, you must include adapter code for HTML, cHTML and WML devices.
Here is what a sample page might look like to use this:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register TagPrefix="mobile" Namespace="System.Web.UI.MobileControls" Assembly="System.Web.Mobile" %>
<%@ Register TagPrefix="acme" Namespace="Acme" Assembly="Acme.TimerFormCS" %>
<html xmlns="http://www.w3.org/1999/xhtml" >
<BODY>
<acme:TimerFormCS Title="HOWDY" runat="server" id="Form1" OnTimer="Form_OnTimer" Delay="2">
<mobile:Panel id="Panel1" runat="server" Wrapping="Wrap" BreakAfter="True">
The current time is <%# DateTime.Now.ToString() %> </mobile:Panel>
<mobile:Panel id="Panel2" runat="Server" Wrapping="Wrap" BreakAfter="True">
This form will refresh in <%# Form1.Delay.ToString() %> seconds.
</mobile:Panel>
<mobile:Command id="Command1" onclick="Command1_Click" runat="server">
Start Timer
</mobile:Command>
</acme:TimerFormCS>
</BODY>
</html>
The above provides you with a Command Button that will set the Delay property of the forms Timer and also change the Text of the Command to "Stop Timer". I haven't used the AutoRedirect property here, but it can easily be set dynamically as well.
At this point, all you need to do is call your method in the Forms OnTimer Handler, set your properties or other display that needs to be Databound, and call the forms DataBind method. Here is the codebehind for my Default.aspx page:
using System.Web.UI.HtmlControls;
using Acme;
public partial class _Default : System.Web.UI.MobileControls.MobilePage
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
((TimerFormCS)this.Form1).Delay = 10000;
Form1.DataBind();
}
}
protected void Form_OnTimer(Object sender, EventArgs e)
{
Form1.DataBind();
}
protected void Command1_Click(object sender, EventArgs e)
{
if (Command1.Text == "Start Timer")
{
((TimerFormCS)this.Form1).Delay = 2;
Command1.Text = "Stop Timer";
}
else
{
Command1.Text = "Start Timer";
((TimerFormCS)this.Form1).Delay = 10000;
Form1.DataBind();
}
}
}The moral of the story? Don't reinvent the wheel. Search first, find something that is close to what you need, and use it as a base. There is "no shame" in using somebody else's code, when it is a sample that is intended to be used freely. That's just using your noodle.
NOTE: Don't forget the adapter declarations in the web.config, or you won't be doing much refreshing!
You can download the complete
Visual Studio 2005 source solution here.