Aaron Goldenthal

Sometimes ASP.NET is Rocket Science

A Custom LinkButton for the ASP.NET Calendar

A couple of weeks ago I posted an article on creating a dynamic button in an ASP.NET Calendar.  The one aspect of that solution that wasn’t quite as clean as I would have liked is how the data key for the button that was clicked was determined.  To remedy that, I’ve created a custom server control that inherits from LinkButton, but provides some customization for this particular application. Specifically, this control: Retains all functionality of the original LinkButton Adds a CalendarClick event that is raised when a button in a Calendar is clicked Within the CalendarClick event, passes the data key of the clicked button in the event args Nothing really monumental, but I thought it was a good discussion on creating a relatively simple custom server control with a new event and EventArgs. Step 1 - CalendarClickEventArgs Building from the bottom up, we’ll first create a class that will provide data for our new event.  In this case, that’s CalendarClickEventArgs. 1: public class CalendarClickEventArgs : EventArgs 2: { 3: public CalendarClickEventArgs() 4: { 5: } 6:  7: public CalendarClickEventArgs(object dataKey) 8: { 9: this.DataKey = dataKey; 10: } 11:  12: public object DataKey { get; set; } 13: } This class inherits from EventsArgs, which is the base class for all event argument classes.  There’s one public property, DataKey, that provides the key identifying the button within the Calendar that was clicked, and a couple of constructors. Step 2 - CalendarClickEventHandler Next, we’ll create an event handler for our new event. 1: public delegate void CalendarClickEventHandler (object sender, 2: CalendarClickEventArgs e); Here we’re simply defining a new delegate with the appropriate signature.  Specifically, a method handling this event should accept an object (the sender), and an instance of CalendarClickEventArgs. Step 3 - CalendarLinkButton Finally, we’ll implement the CalendarLinkButtonClass itself.  To start with, we’ll create a class that inherits from LinkButton. 1: [ 2: DefaultEvent("CalendarClick"), 3: ToolboxData("<{0}:CalendarLinkButton runat=\"server\">CalendarLinkButton</{0}:CalendarLinkButton>") 4: ] 5: public class CalendarLinkButton : LinkButton 6: { 7: } The DefaultEvent attribute specifies the default event for the control (i.e. the event that’s added if you double click on the control in design view).  The ToolBoxData attribute specifies the markup that’s generated when you add the control from the toolbox. Next we’ll add an event to raise when the calendar is clicked.  We’re using the Events collection (inherited from control) to hold delegates for the event, which is a pretty standard convention. 1: public event CalendarClickEventHandler CalendarClick 2: { 3: add 4: { 5: base.Events.AddHandler(EventCalendarClick, value); 6: } 7: remove 8: { 9: base.Events.RemoveHandler(EventCalendarClick, value); 10: } 11: } 12:  13: private static readonly object EventCalendarClick; We’ll also define a method to raise the events.  This method will simply call any applicable delegates for this event. 1: protected virtual void OnCalendarClick(CalendarClickEventArgs e) 2: { 3: CalendarClickEventHandler handler = 4: (CalendarClickEventHandler)base.Events[EventCalendarClick]; 5: if (handler != null) 6: { 7: handler(this, e); 8: } 9: } Finally, we need to implement a mechanism to raise the event.  To accomplish this, we’ll override the RaisePostBackEvent method, which is called by the Page when processing a postback.  Since we want to retain all of the functionality of the original LinkButton, we’ll first call the LinkButton’s RaisePostBackEvent.  This call will: Perform event validation (which includes the event argument, so we get the validation for our Calendar button events) Call the Page.Validate() method if the button causes validation Raise the LinkButton’s Click event Raise the LinkButton’s Command event After that, we’ll raise our new event – CalendarClick.  Recall from my original post on dynamic buttons in a calendar that the __EVENTARGUMENT form parameter is being used to identify the Calendar button that was clicked.  The CalendarClick event is only raised if an event argument is specified, which should always be true when triggered from the Calendar, but wouldn’t be true if the button itself were clicked.  The Page takes care of pulling __EVENTARGUMENT  and passing it to the RaisePostBackEvent method, so that value is passed to the CalendarClickEventArgs. 1: protected override void RaisePostBackEvent(string eventArgument) 2: { 3: base.RaisePostBackEvent(eventArgument); 4:  5: if (!String.IsNullOrEmpty(eventArgument)) 6: { 7: this.OnCalendarClick(new CalendarClickEventArgs(eventArgument)); 8: } 9: } Below is the complete code for CalendarLinkButton. 1: [ 2: DefaultEvent("CalendarClick"), 3: ToolboxData("<{0}:CalendarLinkButton runat=\"server\">CalendarLinkButton</{0}:CalendarLinkButton>") 4: ] 5: public class CalendarLinkButton : LinkButton 6: { 7: protected override void RaisePostBackEvent(string eventArgument) 8: { 9: base.RaisePostBackEvent(eventArgument); 10:  11: if (!String.IsNullOrEmpty(eventArgument)) 12: { 13: this.OnCalendarClick(new CalendarClickEventArgs(eventArgument)); 14: } 15: } 16:  17: protected virtual void OnCalendarClick(CalendarClickEventArgs e) 18: { 19: CalendarClickEventHandler handler = 20: (CalendarClickEventHandler)base.Events[EventCalendarClick]; 21: if (handler != null) 22: { 23: handler(this, e); 24: } 25: } 26:  27: public event CalendarClickEventHandler CalendarClick 28: { 29: add 30: { 31: base.Events.AddHandler(EventCalendarClick, value); 32: } 33: remove 34: { 35: base.Events.RemoveHandler(EventCalendarClick, value); 36: } 37: } 38:  39: private static readonly object EventCalendarClick; 40: } That’s all there is to the code, which can be downloaded with the example from my original post.  It also includes a sample page with the required changes (which overall are pretty minor).

Create Dynamic Buttons in an ASP.NET Calendar

As you get more familiar with the ASP.NET calendar, eventually you start tweaking it: adding your own styles, using the DayRender event to customize the content of each day, etc.  At some point you may try to dynamically create a button during DayRender, at which point you see the button render on the client, but when it’s clicked, no event fires.  This article will give you a solution to work around this problem. Why Adding Buttons During DayRender Fails Some people who start using more advanced techniques to tweak the Calendar assume initially that it’s similar to the other ASP.NET Data Controls (e.g. Repeater, GridView, ListView) so you can use DayRender on a Calendar like you would use ItemDataBound in a Repeater, or RowDataBound in a GridView.  That is not the case.  The Calendar’s DayRender event is exactly what is says it is - it’s an event fired during the render process, just before the cell for the given day is rendered.  Because this event is fired so late in the page life cycle, you cannot add controls that fire events.  From the Calendar documentation: Because the DayRender event is raised while the Calendar control is being rendered, you cannot add a control that can also raise an event, such as LinkButton. You can only add static controls, such as LiteralControl, Label, Image, and HyperLink. You may search around for another event to use instead, but you won’t find one.  The calendar is only created during the render process, there’s no collection of controls to manipulate earlier.  It was never designed for that kind of manipulation.  There are some commercial controls with more advanced functionality that could be used, but this is a relatively easy (and free) solution for simple scenarios. The Workaround The workaround for this is relatively simple: Add a static control in the DayRender event Make it behave like a dynamic control that has events Pass data to identify which button was clicked. For this example I’ve set up a simple database of activities, and the appropriate activities for each day will be displayed in the Calendar.  First, set up a few controls: 1: <asp:Calendar ID="Calendar1" runat="server" VisibleDate="03/01/2009" 2: CssClass="monthCalendar" BorderStyle="None" 3: DayStyle-CssClass="dayStyle" OtherMonthDayStyle-CssClass="otherMonthStyle" 4: TodayDayStyle-CssClass="todayStyle" DayHeaderStyle-CssClass="dayHeaderStyle" 5: WeekendDayStyle-CssClass="weekendDayStyle" ShowTitle="false" 6: SelectionMode="None" ShowGridLines="false" 7: OnDayRender="Calendar1_DayRender"></asp:Calendar> 8: <asp:LinkButton ID="CalendarLinkButton" runat="server" CssClass="hide" 9: OnClick="CalendarLinkButton_Click"></asp:LinkButton> 10: <asp:Label ID="ResultsLabel" runat="server" CssClass="results"></asp:Label> There’s nothing really special about the Calendar in this case, it’s mostly styling, but I have specified a handler for the DayRender event, which I’ll address shortly.  The significant piece of this markup is the CalendarLinkButton.  This LinkButton will not be visible on the page (the “hide” CssClass sets display: none).  It’s simply a placeholder so we have a LinkButton whose Click event can be fired.  Instead of trying to attach event to LinkButtons in the Calendar, the CalendarLinkButton’s Click event will be fired for each activity link in the Calendar, passing an argument that identifies which activity was clicked. Lastly, the ResultsLabel will simply display a message indicating which activity was clicked, just to show it’s working. Now, on to the code.  First, the Calendar DayRender event handler. 1: protected void Calendar1_DayRender(object sender, DayRenderEventArgs e) 2: { 3: DataClassesDataContext db = new DataClassesDataContext(); 4: var activities = from a in db.Activities where a.Date.Date == e.Day.Date 5: select a; 6: foreach (Activity a in activities) 7: { 8: HyperLink link = new HyperLink(); 9: link.Text = a.Name; 10: link.CssClass = "activity"; 11: link.NavigateUrl = 12: Page.ClientScript.GetPostBackClientHyperlink(CalendarLinkButton, 13: a.ActivityID.ToString(), true); 14: e.Cell.Controls.Add(link); 15: } 16: } I’m using Linq to SQL to pull data for this example, so the DataContext is created and a simple query is run to pull activities associated with the current day from the database.  For each activity that is found, a HyperLink is created and added to the Controls collection of the Calendar table cell.  This HyperLink is the “button” that’s added for each event.  All fairly standard code for adding static events to a Calendar Day. The significant line here is setting the NavigateUrl of the HyperLink.  This is done through the GetPostBackClientHyperlink method which, according to the documentation: Gets a reference, with javascript: appended to the beginning of it, that can be used in a client event to post back to the server for the specified control with the specified event arguments and Boolean indication whether to register the post back for event validation. So, in this case GetPostBackClientHyperlink returns a string equivalent to that used as the href value in the <a> tag rendered for the CalendarLinkButton.  I’ve passed it the ActivityID to be used as the Event Argument, which will allow us to determine which event was clicked.  The last parameter registers the post back for event validation.  Without this set to true, every postback from one of the Calendar’s HyperLinks would result in the following exception: Invalid postback or callback argument. Event validation is enabled using in configuration or <%@ Page EnableEventValidation="true" %>in a page... To see the specifics of how this is rendered, the CalendarLinkButton renders a link like: <a href="javascript:__doPostBack('CalendarLinkButton','')"> and the activity HyperLinks render links like (where the second argument, ‘1’ in this case, is the appropriate ActivityID): <a href="javascript:__doPostBack('CalendarLinkButton','1')"> The ASP.NET Postback Mechanism For those less familiar with how ASP.NET postbacks really work, here’s a little more detail.  On an ASP.NET page, there are two hidden fields that are automatically generated: __EVENTTARGET and __EVENTARGUMENT.  These fields are populated when a postback is initiated on the client.  The Page the uses __EVENTTARGET to determine which control fired the event and call its RaisePostBackEvent method, which will raise the appropriate event (e.g. a Button’s Click event).  Depending on the control, this method may or may not use __EVENTARGUMENT to determine which event was raised (controls will multiple postback events, such as the GridView, will use it). If you view the rendered HTML of your page, you’ll see the following javascript method is automatically generated to populate these values and submit the form: function __doPostBack(eventTarget, eventArgument) { if (!theForm.onsubmit || (theForm.onsubmit() != false)) { theForm.__EVENTTARGET.value = eventTarget; theForm.__EVENTARGUMENT.value = eventArgument; theForm.submit(); } } In this example, I'm using the event argument as a way to pass data to identify which button was clicked since the LinkButton itself does not use it. So, when the activity links are clicked, they will cause a post back that triggers the CalendarLinkButton’s Click event.  Within this event, the ActivityID can be retrieved from the __EVENTARGUMENT hidden field (through the Request.Form collection).  All of this can be seen in the handler for the CalendarLinkButton’s Click event handler, which simply takes the ActivityID, pulls relevant data from the database, and writes a status message to ResultsLabel. 1: protected void CalendarLinkButton_Click(object sender, EventArgs e) 2: { 3: int id = Convert.ToInt32(Request.Form["__EVENTARGUMENT"]); 4:  5: DataClassesDataContext db = new DataClassesDataContext(); 6: var activity = (from a in db.Activities where a.ActivityID == id 7: select a).Single(); 8:  9: ResultsLabel.Text = String.Format("You selected {0} on {1}", 10: activity.Name, activity.Date.ToShortDateString()); 11: } The screenshot below shows the final results. Download sample website: CalendarButtonEvent.zip (567 kb) Updated April 11, 2009: The attached sample website was updated to include an example with a custom LinkButton that simplifies the click event handling. See details here.