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