Monday, 30 January 2012

How to use jsTree in ASP.NET Web Forms

jsTree is a free AJAX based tree-view component for jQuery that, with its rich feature set and plug-in architecture, appears to be a perfect candidate for use in an ASP.NET Web Control.

For my purposes I needed an asynchronous tree-view control that allowed me to selectively include checkboxes and to maintain state across post backs. On the face of it jsTree appeared to tick all of the boxes but off-the-bat I discovered that, although state could be applied through the use of cookies, state couldn’t be applied to the instance of the page and it appeared that checkbox state was a bit of a misnomer.

That said, it is undoubtedly a great plug-in with a lot of potential and certainly with enough potential for me to stick with it. The control that I had in mind was one in which the tree and its nodes would be specified in similar terms to the standard ASP.NET TreeView and then consumed by jsTree using JSON. Finally, the state of the tree and its checkboxes would be maintained in the page.

The final result is a tree-view control that syndicates the sophisticated features of jsTree into a simple and easy to deploy ASP.NET Web Control.

To include this control on your ASP.NET Web Page download the JsTreeView binary or source code from http://code.zyky.com/jsTreeView/, it is available as a .NET 2.0, or a .NET 3.5 binary, depending on your requirement and then install it to the Bin directory of your site.

Add a reference to the control in your page:
<%@ Register assembly="JsTreeView" namespace="Custom.Web.UI.WebControls" tagprefix="custom"%>

As this is a jQuery plug-in you'll need to add a link to the jQuery library within the head tags of your web page:
<head runat="server">
    <title>jsTreeView</title>
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</head>
Ensure that you include a ScriptManager in your page:
<asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>

Add the control to your page:
<custom:jsTreeView ID="jsTreeView1" runat="server"
            ServiceUrl="jsTreeSvc.asmx/GetNodes"
            Theme="Default"
            CheckBoxes="true"
            CssClass="jsTreePanel"></custom:jsTreeView>

You’ll note that I have only covered a small number of the properties available in jsTree (just those that meet my current requirement). To use the control simply populate the ServiceUrl property with a path to your web service, set the theme and use checkboxes as required.

Finally you will need to add a web service to create and return the tree view nodes that jsTree will display. To do this set up an appropriate web service (asmx) page, create a new instance of jsTree to return your nodes and then populate it with your tree nodes:
jsTree tree = new jsTree();
jsTreeNode node1 = new jsTreeNode(treeId, "1", "The Good", "#", true);

tree.Nodes.Add(node1);

return tree.Json();

Demo

View a live demo or download the source code at http://code.zyky.com/jsTreeView/

Trails

Thursday, 9 June 2011

Custom SEO Friendly Paging with ASP.NET Repeater and SqlDataSource

One of my favourite things about ASP.NET is the number of ways that you can rub the lamp to summon a new genie. Take paging for example, the ability to move through pages of data using Next, Previous, First, Last and numeric page number buttons is present in the GridView and ListView controls but not so in the Repeater control. One approach to adding paging to the Repeater control is the PagedDataSource method that adds the default paging mechanism to the control.

While the PagedDataSource is useful in many situations, its default paging mechanism relies on retrieving all of the data on each and every request. Furthermore, it works by posting the request back to the page which means that search engines will not be able to index beyond the first page of data.

It is possible to make retrieval much more efficient by leveraging the ROW_NUMBER() feature of SQL Server and configuring an ObjectDataSource for custom paging (see Scott Mitchell’s article Custom Paging in ASP.NET 2.0 with SQL Server 2005) however there is no such direct means if you are using the SqlDataSource (a two-tier application architecture that's faster to implement than the three-tier ObjectDataSource, and one that I use extensively).

It is also now also possible (using ASP.NET 3.5) to add search engine friendly paging by combining a ListView control with a DataPager (instructing it to generate an SEO-friendly paging interface) and although this is a landmark development, if you're not using the ListView control or if you need more control over your paging behaviour (or you're simply using ASP.NET 2.0) the good news is that it's quite easy to roll your own reusable SEO-friendly paging control.

This article shows you how to implement super-efficient, google-style SEO-friendly paging using the Repeater control, the SqlDataSource control and custom paging.

First up, we have a stored procedure that will return our filtered, sorted and paged result set along with a count of the total number of matches:
USE [SqlPagedDataSource]
GO
/****** Object:  StoredProcedure [dbo].[usp_SampleData_GetData]    Script Date: 06/09/2011 14:51:22 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[usp_SampleData_GetData]

 @StartRow int,
 @PageSize int,
 @Sort varchar(50) = 'Name',
 @SortDirection varchar(50) = 'Asc',
 @Search varchar(255) = NULL,

 @RowCount int = NULL OUTPUT

AS
BEGIN

 SET @RowCount = (
 SELECT
  COUNT(*)
 FROM
  [dbo].[SampleData]
 WHERE
  (@Search IS NULL OR
   (@Search IS NOT NULL AND
    ([Name] LIKE '%' + @Search + '%' OR
     [Phone] LIKE '%' + @Search + '%' OR
     [Email] LIKE '%' + @Search + '%'
    )
   )
  )
 )

 SELECT * FROM
  (SELECT ROW_NUMBER() OVER(ORDER BY 
   ( CASE WHEN @Sort = 'Name' And @SortDirection = 'Asc' THEN [Name] END ) ASC,
   ( CASE WHEN @Sort = 'Name' And @SortDirection = 'Desc' THEN [Name] END ) DESC,
   ( CASE WHEN @Sort = 'Phone' And @SortDirection = 'Asc' THEN [Phone] END ) ASC,
   ( CASE WHEN @Sort = 'Phone' And @SortDirection = 'Desc' THEN [Phone] END ) DESC,
   ( CASE WHEN @Sort = 'Email' And @SortDirection = 'Asc' THEN [Email] END ) ASC,
   ( CASE WHEN @Sort = 'Email' And @SortDirection = 'Desc' THEN [Email] END ) DESC) As RowNum
   ,[Name]
   ,[Phone]
   ,[Email]
  FROM
   [dbo].[SampleData]
  WHERE
   (@Search IS NULL OR
    (@Search IS NOT NULL AND
     ([Name] LIKE '%' + @Search + '%' OR
      [Phone] LIKE '%' + @Search + '%' OR
      [Email] LIKE '%' + @Search + '%'
     )
    )
   )
  ) T
  WHERE
   RowNum BETWEEN @StartRow AND (@StartRow + (@PageSize - 1))

END

Next, to access the data from the database we're going to build on the SqlDataSource control by adding some properties and extending its behaviour to support our SEO-friendly custom paging. By creating a new ‘SqlPagedDataSource’ web server control (that inherits from SqlDataSource) and directly integrating its ‘Selecting’ and ‘Selected’ events we can insert, capture and then expose the information that makes paging possible – such as inserting the current page size, handling the page number being requested and capturing the total number of known matches.

The SqlPagedDataSource control then exposes the literal pager content to any control that wants to consume it (such as our SqlPagedDataSourcePager which we'll come on to later).

Use the SqlPagedDataSource control by declaring it as you would the ordinary SqlDataSource and then specify the PageSize, PageSetLength, SortName and SortDirection properties as required:
<zyky:SqlPagedDataSource ID="pds1" runat="server"
    ProviderName="System.Data.SqlClient"
    ConnectionString="<%$ ConnectionStrings:ConnStr %>"
    CancelSelectOnNullParameter="false"
    SelectCommand="usp_SampleData_GetData"
    SelectCommandType="StoredProcedure"
    PageSize="5"
    PageSetLength="10"
    SortName="Name"
    SortDirection="Asc">
    <SelectParameters>
    </SelectParameters>
</zyky:SqlPagedDataSource>

Then use the DataSourceID property to bind your databound control to the SqlPagedDataSource:
<asp:Repeater ID="Repeater1" runat="server" DataSourceID="pds1">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li>
            <asp:Label ID="NameDataLabel" runat="server" Text='<%#Eval("Name")%>'></asp:Label>,
            <asp:Label ID="PhoneDataLabel" runat="server" Text='<%#Eval("Phone")%>'></asp:Label>,
            <asp:Label ID="EmailDataLabel" runat="server" Text='<%#Eval("Email")%>'></asp:Label>
        </li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>

Then we have the pager itself ‘SqlPagedDataSourcePager’, it's a simple web control that you can hook up to the SqlPagedDataSource via the ‘DataSourceID’ property to render the contents of the pager on the page:
<zyky:SqlPagedDataSourcePager ID="SqlPagedDataSourcePager1" runat="server" DataSourceID="pds1"></zyky:SqlPagedDataSourcePager>

The control itself has a really tiny footprint and simply inherits the Literal control to render its output:
Imports Microsoft.VisualBasic

Namespace Zyky

    Public Class SqlPagedDataSourcePager

        Inherits System.Web.UI.WebControls.Literal

        Public Property DataSourceID() As String
            Get
                Return CType(ViewState("DataSourceID"), String)
            End Get
            Set(ByVal value As String)
                ViewState("DataSourceID") = value
            End Set
        End Property

        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
            Dim ds As Zyky.SqlPagedDataSource = Page.FindControl(DataSourceID)
            If Not ds Is Nothing Then
                writer.Write(ds.Contents)
            End If
            MyBase.Render(writer)
        End Sub

    End Class

End Namespace

Finally, we can output some information about the results such as the current page, the number of pages or the number of results by reading off the properties that we exposed in our SqlPagedDataSource control.

I chose to write a simple templated control to display this information:
<zyky:SqlPagedDataSourceInfoPanel ID="InfoPanel1" runat="server" DataSourceID="pds1">
    <ResultTemplate>
        Page
        <zyky:SqlPagedDataSourceInfo ID="Info1" runat="server" DataSourceID="pds1" PropertyName="CurrentPageQuery" />
        of
        <zyky:SqlPagedDataSourceInfo ID="Info2" runat="server" DataSourceID="pds1" PropertyName="PageCount" />
        for
        <zyky:SqlPagedDataSourceInfo ID="Info3" runat="server" DataSourceID="pds1" PropertyName="Results" />
        Results
    </ResultTemplate>
    <NoResultTemplate>
        << No Results >>
    </NoResultTemplate>
</zyky:SqlPagedDataSourceInfoPanel>

There you have it, a completely reusable seo-friendly custom paging interface for just about any type of databound control that you can think of. Now that’s magic!

Demo

View a live demo at http://code.zyky.com/sqlpageddatasource/

Code

Download the Source Code

Trails

Saturday, 14 May 2011

CRUD ASP.NET and SQL Script Generator

This is the first post in a little while (up-to-my-eyes lately) - anyway, let's get down to business.

Today (like many other days) I find myself wiring the four basic functions of persistent storage – Create, Read, Update and Delete (CRUD) – into my ASP.NET forms application. It's integral to what we do as developers...but it's tedious...really tedious...and so rather than bob along in this arid sea of tranquillity I wondered if there was something that could do this for me? The answer is 'yes' there probably is – but that might not be as flexible, simple (or half as much fun) as rolling my own little ASP.NET and SQL Script Generator.

What I wanted was a really simple way of typing in the field names, selecting the desired controls and having the create table, the stored procedures and the ASP.NET controls all created for me; then I wanted to be able to save the definition and load it all back in again if I needed to make any changes or if I wanted to come back to it later - and I wanted to do all of this in under a day! Thankfully, developing a CRUD ASP.NET and SQL script generator turned out to be a whole lot easier than I imagined, by catering to ASP.NET with a dash of XML and a sprinkle of XSLT it was baked in next to no time.

The interface allows you to input a table name and select a template (this points to an XSLT template to create the output). Clicking on the 'Load Sample' link below the 'Definition' heading will load a sample definition along with its output.

Once you enter a definition into the grid, either by typing it in or by uploading one that you created earlier, just select a template and click on the generate files button to see the SQL and ASP.NET created automatically; then simply execute the SQL script against your database and copy the ASP.NET into your page.


This works by converting your input into XML and then translating it using XSLT templates in just a few lines of code; the same XML methods allow you to download or upload the XML description and regenerate your scripts just as easily.

You can use this online at http://code.zyky.com/crudgenerator/ or download the source code and add your own templates. Give it a go, it's saved me oodles of time already.

Demo

View a live demo at http://code.zyky.com/crudgenerator/

Code

Download the Source Code

Tuesday, 1 March 2011

Dynamic Loading of ASP.NET User Controls

In this article we'll look at how to dynamically load and unload web user controls on an ASP.NET page and then page through the selected controls.

Why? I hear you ask; well, my need was for a referral system in which a user could select one or more agencies to refer a job to and then complete each of the agency-specific forms (which in turn are connected to their own data sources). The challenge was on to link-up the user selections with the various forms in a simple end-to-end manner and then have each of the forms handle their own bit of data at the end of the process.

To demonstrate this, we'll create a clutch of user controls to load on demand, another control to take the user selections and then finally wrap it all up in an ASP.NET page to handle the paging.

First create a set of user controls that you want to make available to your user:
<%@ Control Language="VB" AutoEventWireup="false" CodeFile="Car.ascx.vb" Inherits="Car" %>
<h1>Car</h1>

<asp:Label ID="CarLabel" runat="server" AssociatedControlID="CarList" Text="Car:"></asp:Label>
<asp:DropDownList ID="CarList" runat="server">
    <asp:ListItem Value="Rolls-Royce" Text="Rolls-Royce"></asp:ListItem>
    <asp:ListItem Value="Bentley" Text="Bentley"></asp:ListItem>
</asp:DropDownList>

Then create a selector control so that the user can choose which of the controls to be presented with:
<%@ Control Language="VB" AutoEventWireup="false" CodeFile="Selector.ascx.vb" Inherits="Selector" %>
<h1>Acme Party Services</h1>
<asp:Label ID="SelectorLabel" runat="server" AssociatedControlID="SelectorList" Text="Please select your party services:"></asp:Label>
<asp:CheckBoxList ID="SelectorList" runat="server">
    <asp:ListItem Value="Car" Text="Car"></asp:ListItem>
    <asp:ListItem Value="Dress" Text="Dress"></asp:ListItem>
    <asp:ListItem Value="Suit" Text="Suit"></asp:ListItem>
</asp:CheckBoxList>

In the code behind of the selector control expose the user selections through a property:
Partial Class Selector
    Inherits System.Web.UI.UserControl

    Public ReadOnly Property SelectedControlNames() As ArrayList
        Get
            Return GetSelectedControlNames()
        End Get
    End Property

    Private Function GetSelectedControlNames() As ArrayList

        Dim al As New ArrayList

        For Each li As ListItem In SelectorList.Items
            If li.Selected Then
                al.Add(li.Value)
            End If
        Next

        Return al

    End Function

End Class

Finally, create the page to host the user controls along with the paging mechanism:
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>
<%@ Register TagPrefix="uc" TagName="Selector" Src="~/Selector.ascx" %>
<%@ Register TagPrefix="uc" TagName="Car" Src="~/Car.ascx" %>
<%@ Register TagPrefix="uc" TagName="Dress" Src="~/Dress.ascx" %>
<%@ Register TagPrefix="uc" TagName="Suit" Src="~/Suit.ascx" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Control Pager</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
        <%-- PlaceHolder to wrap the dynamic user controls --%>        
        <asp:PlaceHolder ID="PlaceHolder1" runat="server">
        
            <%-- Declare the static 'Selector' user control --%>            
            <uc:Selector ID="Selector1" runat="server" />
        
        </asp:PlaceHolder>
        
        <br />
        
        <%-- Buttons for paging through the controls --%>        
        <asp:Button ID="BackButton" runat="server" Text="&lt; Back" Visible="false" />
        <asp:Button ID="NextButton" runat="server" Text="Next &gt;" />
    
    </div>
    </form>
</body>
</html>

The code behind shows that we start by loading all of the controls from the control history (using an array list held in view state), this is because dynamic controls do not survive page post backs on their own and must be reloaded in exactly the same order in which they were last provisioned. Next we load up any new controls that have been selected and then later in the page life cycle, if the selection has changed, unload any controls that are no longer required. Finally, using the 'Back' and 'Next' buttons we can display each control in the sequence:
Partial Class _Default
    Inherits System.Web.UI.Page

    ' this property maintains a history of loaded controls
    Public Property SelectedControls() As ArrayList
        Get
            Dim o As Object = ViewState("SelectedControls")
            If o Is Nothing Then
                Return New ArrayList
            End If
            Return CType(ViewState("SelectedControls"), ArrayList)
        End Get
        Set(ByVal value As ArrayList)
            ViewState("SelectedControls") = value
        End Set
    End Property

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        LoadControls()

    End Sub

    Private Sub LoadControls()

        ' Maintain the control indexes and viewstate by loading the
        ' control history plus any new selections and then drop any 
        ' de-selected controls later in the page life cycle.

        ' Read in control history
        Dim controlHistory As ArrayList = SelectedControls

        ' Load controls
        For Each controlItem As String In controlHistory
            Dim uc As UserControl = LoadControl("~/" & controlItem & ".ascx")
            uc.ID = controlItem
            PlaceHolder1.Controls.Add(uc)
        Next

        ' Read in the selected control names from the selector control
        Dim controlList As ArrayList = Selector1.SelectedControlNames

        ' Load new selections
        For Each controlItem As String In controlList

            If Not controlHistory.Contains(controlItem) Then

                Dim uc As UserControl = LoadControl("~/" & controlItem & ".ascx")
                uc.ID = controlItem

                PlaceHolder1.Controls.Add(uc)

                ' It matters to viewstate that properties are added after 
                ' the control has been added to the control hierarchy
                uc.Visible = False

                controlHistory.Add(controlItem)

            End If
        Next

        ' Update the control history
        SelectedControls = controlHistory

    End Sub

    Private Sub CleanUpControlHistory()

        Dim controlHistory As ArrayList = SelectedControls
        Dim controlHistoryCopy As ArrayList = controlHistory.Clone

        ' Read in the selected control names from the selector control...
        Dim selectedControlNames As ArrayList = Selector1.SelectedControlNames

        ' ...and compare them to the control history
        For Each controlItem As String In controlHistoryCopy

            If Not selectedControlNames.Contains(controlItem) Then

                ' Drop unused control
                Dim uc As UserControl = PlaceHolder1.FindControl(controlItem)
                PlaceHolder1.Controls.Remove(uc)
                controlHistory.Remove(controlItem)
            End If
        Next

        ' Update the control history
        SelectedControls = controlHistory

    End Sub

    Protected Sub NextButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles NextButton.Click

        ' Clean up control history
        If Selector1.Visible Then CleanUpControlHistory()

        Move(1)

    End Sub

    Protected Sub BackButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles BackButton.Click

        Move(-1)

    End Sub

    Private Sub Move(ByVal iDirection As Integer)

        Dim ctrlTop As Integer = 0
        Dim ctrlBottom As Integer = PlaceHolder1.Controls.Count - 1

        ' Iterate through the control collection
        For i As Integer = ctrlTop To ctrlBottom

            If ((iDirection = -1 And i > ctrlTop) Or (iDirection = 1 And i < ctrlBottom)) Then

                Dim uc As UserControl = PlaceHolder1.Controls(i)

                ' Position on visible control
                If uc.Visible Then

                    Dim nxt As Integer = (i + iDirection)

                    ' Change view
                    uc.Visible = False
                    CType(PlaceHolder1.Controls(nxt), UserControl).Visible = True
                    BackButton.Visible = (nxt > 0)

                    Exit For
                End If
            End If
        Next

    End Sub

End Class

There you have it, a web user control loader and pager. Enjoy.

Demo

View a live demo at http://code.zyky.com/controlpager

Code

Download the Source Code

Trails

Friday, 11 February 2011

Bird Seed (A Simple Tweet Reader)

This post shows how to easily integrate twitter updates into your ASP.NET web site using LINQ to XML and the Repeater control.

Twitter has a flexible collection of mechanisms for accessing information in a variety of formats such as a simplified RSS style XML feed or a plain XML feed that provides a richer set of information about a user and their updates. I am going to use the latter, although for brevity I am just going to pull the date, message and user from the user_timeline method which can be retrieved from http://twitter.com/statuses/user_timeline/user_id.xml (replacing user_id with your twitter handle). To read this information I am going to use LINQ to XML (available in the .NET Framework Version 3.5) as this API simplifies working with XML data without having to use additional language syntax like XPath. Finally, to format the output I am going to bind the data to a Repeater Control (which will reside in a web user control that we can set properties on).

Create a new web user control:

<%@ Control Language="VB" AutoEventWireup="false" CodeFile="BirdSeed.ascx.vb" Inherits="BirdSeed" %>

<asp:Repeater ID="Repeater1" runat="server">
    <ItemTemplate>
        <p>
            <asp:Label ID="NameLabel" runat="server" Text='<%#Eval("Name")%>'></asp:Label>
            <br />
            <asp:Label ID="DateCreatedLabel" runat="server" Text='<%#Eval("DateCreated", "{0:dd/MM/yyyy HH:mm}")%>'></asp:Label>
            <br />
            <asp:Label ID="MessageLabel" runat="server" Text='<%#Eval("Message")%>'></asp:Label>
        </p>
    </ItemTemplate>
</asp:Repeater>

Add a UserID property to the user control's code behind and launch the request in its Page_Load method:

Imports System.Net
Imports System.Xml
Imports System.Xml.Linq
Imports System.Linq

Partial Class BirdSeed
    Inherits System.Web.UI.UserControl

    Dim _UserID As String = Nothing
    Public Property UserID() As String
        Get
            Return _UserID
        End Get
        Set(ByVal value As String)
            _UserID = value
        End Set
    End Property

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        If Not IsPostBack Then

            Dim url As String = String.Format("http://twitter.com/statuses/user_timeline/{0}.xml", UserID)

            Using wr As WebResponse = WebRequest.Create(url).GetResponse()

                Using reader As XmlReader = XmlReader.Create(wr.GetResponseStream())

                    Dim xDoc As XDocument = XDocument.Load(reader)

                    Dim query = From el In xDoc.Descendants("status") _
                                Select New With { _
                                    .DateCreated = ParseDateTime(GetValue(el, "created_at")), _
                                    .Message = ParseLinks(GetValue(el, "text")), _
                                    .Name = GetUserName(el) _
                                }

                    Repeater1.DataSource = query.ToList
                    Repeater1.DataBind()

                End Using

            End Using

        End If

    End Sub

    Private Function GetUserName(ByVal element As XContainer) As String

        Dim query = From el In element.Descendants("user") _
                    Select New With { _
                        .Name = GetValue(el, "name") _
                    }

        Return query.ToList.Item(0).Name

    End Function

    Private Function GetValue(ByVal element As XContainer, ByVal key As String) As String
        Dim el = element.Element(key)
        Return If(el Is Nothing, "", el.Value)
    End Function

    Public Shared Function ParseDateTime(ByVal dt As String) As DateTime

        Dim dayOfWeek As String = dt.Substring(0, 3).Trim()
        Dim month As String = dt.Substring(4, 3).Trim()
        Dim dayInMonth As String = dt.Substring(8, 2).Trim()
        Dim time As String = dt.Substring(11, 9).Trim()
        Dim offset As String = dt.Substring(20, 5).Trim()
        Dim year As String = dt.Substring(25, 5).Trim()

        Return DateTime.Parse(String.Format("{0}-{1}-{2} {3}", dayInMonth, month, year, time))

    End Function

    Private Function ParseLinks(ByVal text As String) As String

        Dim pattern As String = "(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?"

        Dim matchDelegate As New MatchEvaluator(AddressOf LinkMatchHandler)
        Dim regx As Regex = New Regex(pattern, RegexOptions.IgnoreCase)

        Return regx.Replace(text, matchDelegate)

    End Function

    Private Function LinkMatchHandler(ByVal m As Match) As String
        Return String.Format("<a href=""{0}"">{0}</a>", m)
    End Function

End Class

Finally, register the user control on your application page and make an instance of it (passing along a twitter handle in the UserID property).

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>
<%@ Register TagPrefix="uc" TagName="BirdSeed" Src="~/BirdSeed.ascx" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Bird Seed</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
        <uc:BirdSeed ID="BirdSeed1" runat="server" UserID="dgzyky" />
    
    </div>
    </form>
</body>
</html>

You can limit the number of results by simply setting the repeaters data source like so:

Repeater1.DataSource = query.Take(5).ToList

There you have it, a simple tweet reader using ASP.NET, LINQ to XML and the Repeater Control.

Demo

View a live demo at http://code.zyky.com/birdseed/

Code

Download the Source Code

Tuesday, 8 February 2011

Capture Page Event in Web User Control

This post is about how to raise an event in an ASP.NET page and capture or handle the event in a web user control.

Today I was tasked with developing a web application that would load and daisy-chain a user-defined selection of ASP.NET forms and, at the end of the cycle, command each form to update its respective data source. The approach that I took was to create each form as a web user control that contained a form view control bound to a declarative data source for its select, insert and update operations.

This was easy enough but I still needed a way to command each form to update its data source and the options appeared to be either to create a public method in each of the user controls and call that in the page (which didn't feel like good design) or to create an event handler for the user control and use that in the page. The caveat with the second method is that event bubbling starts at the bottom (the user control) and travels up to the top level (the page) – this is groovy if you want to handle a user control event in the parent page – I needed however to raise an event in the parent page and then have each user control respond automatically to the event by saving its data.

To achieve this I used a custom base page class in ASP.Net that inherited from page and included a new 'ButtonClick' event that I could later raise in my main application page:

Imports Microsoft.VisualBasic

Namespace Custom

    Namespace Web

        Namespace UI

            Public Class Page

                Inherits System.Web.UI.Page

                Protected Sub OnButtonClick(ByVal e As EventArgs)
                    RaiseEvent ButtonClick(Me, e)
                End Sub

                Public Event ButtonClick As EventHandler

            End Class

        End Namespace

    End Namespace

End Namespace

Then in my application page I swapped out the System.Web.UI.Page inheritance with my Custom.Web.UI.Page reference and captured a button click that would raise the event:

Partial Class MyForms

    Inherits Custom.Web.UI.Page

    Protected Sub SaveButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles SaveButton.Click
        ' Raise event
        OnButtonClick(e)
    End Sub

    ' ...

End Class

Following on from this I hooked up an event handler in each of the web user controls to listen for the event and save the form's data:

Partial Class Controls_EspnForm
    Inherits System.Web.UI.UserControl

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        Dim p As Custom.Web.UI.Page = CType(Page, Custom.Web.UI.Page)

        AddHandler p.ButtonClick, AddressOf Page_ButtonClick

    End Sub

    Protected Sub Page_ButtonClick(ByVal sender As Object, ByVal e As System.EventArgs)

        ' Do something here...

    End Sub

End Class

There we have it, now I could fire the insert and update commands (or execute any other code) on each and every web user control by simply firing my new 'ButtonClick' event.

Creating a custom event type and adding an argument


To send along an argument with the event simply create a custom event type (I added it to my Custom.Web.UI namespace):

Namespace Custom

    Namespace Web

        Namespace UI

            Public Class CustomEventArgs

                Inherits EventArgs

                Public EventArgument As String

                Public Sub New(ByVal arg As String)
                    Me.EventArgument = arg
                End Sub

            End Class

' ...

Raise the event along with the argument:

Partial Class MyForms

    Inherits Custom.Web.UI.Page

    Protected Sub SaveButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles SaveButton.Click

        Dim arg As Custom.Web.UI.CustomEventArgs = New Custom.Web.UI.CustomEventArgs("My Argument")

        ' Raise event
        OnButtonClick(arg)

    End Sub

    ' ...

End Class

Then just read off the argument in the Page_ButtonClick method:

Protected Sub Page_ButtonClick(ByVal sender As Object, ByVal e As System.EventArgs)

    Dim args As Custom.Web.UI.CustomEventArgs = CType(e, Custom.Web.UI.CustomEventArgs)

    Dim value As String = args.EventArgument

End Sub

Saturday, 29 January 2011

ASP.NET Elephant - ASP.NET Tips, Tricks, Tales and Trails

This is my first post on ASP.NET Elephant - ASP.NET Tips, Tricks, Tales and Trails. So Why the Elephant? Well I don't have a memory like an elephant and have long realised that I need one - so welcome to my ASP.NET 'social memory'. Every now and then I'll be posting ASP.NET tips and tricks, developer tales, and resource trails.