Setting DateTime DependencyProperty in XAML

January 9, 2009 05:03 by wjchristenson2

Sometimes when setting properties with XAML you may get the AG_E_PARSER_BAD_PROPERTY_VALUE error.  This can mean a variety of things, but in this post we are going to visit a situation where .NET cannot convert the XAML string to the .NET object's type.

A good example of this is a Date property.  Let's say we have a control that has a MaxDate property.  If we set the property in XAML it may look like this: MaxDate="01-01-2009".  Unfortunately this does not work.  You'll get our fatal parser error because the parser is not smart enough to parse the string to a date type.

The workaround is to hold .NET parsers hand and tell it how to parse the string to our desired type (in this case a Date).  We'll do this by first creating our own TypeConverter.  I'll call it the DateTypeConverter.  We'll inherit from the TypeConverter base class and override key methods used to detect if we can convert from/to a type and handle the actual conversion process.  Here's our TypeConverter class for converting from and to a Date type.

Imports System.ComponentModel

Public Class DateTypeConverter
    Inherits TypeConverter

    Public Overrides Function CanConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal sourceType As System.Type) As Boolean
        If sourceType.Equals(GetType(String)) Then
            Return True
        Else
            Return MyBase.CanConvertFrom(context, sourceType)
        End If
    End Function

    Public Overrides Function CanConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As System.Type) As Boolean
        If destinationType.Equals(GetType(String)) Then
            Return True
        Else
            Return MyBase.CanConvertTo(context, destinationType)
        End If
    End Function

    Public Overrides Function ConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object) As Object
        If TypeOf value Is String Then
            Try
                Return Date.Parse(value.ToString())
            Catch
                Throw New InvalidCastException(value)
            End Try
        Else
            Return MyBase.ConvertFrom(context, culture, value)
        End If
    End Function

    Public Overrides Function ConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As System.Type) As Object
        If destinationType.Equals(GetType(String)) Then
            Return value.ToString()
        Else
            Return MyBase.ConvertTo(context, culture, value, destinationType)
        End If
    End Function
End Class

Now that we have our custom converter created, we need to tell our control's property to use this converter when being set via a property attribute.  In our case, when the parser attempts to set the property, it will invoke our DateTypeConverter and parse the string to a date properly.

Public Shared ReadOnly MaxDateProperty As DependencyProperty = DependencyProperty.Register("MaxDate", GetType(Date), GetType(SilverlightControl1), New PropertyMetadata(Date.MaxValue, AddressOf MaxDateChangedHandler))

<TypeConverter(GetType(DateTypeConverter))> _
      Public Property MaxDate() As Date
    Get
        Return DirectCast(GetValue(SilverlightControl1.MaxDateProperty), Date)
    End Get
    Set(ByVal value As Date)
        SetValue(SilverlightControl1.MaxDateProperty, value)
    End Set
End Property

Take note of line 3.  Adding the TypeConverter attribute tells the .NET XAML parser to use our DateTypeConverter to parse the String to Date.

SiverlightDateTime_Soln.zip (593.57 kb)

Bookmark and Share

Silverlight Global Properties

January 8, 2009 08:16 by wjchristenson2

It took me awhile to figure out the best way to create objects that can be used application-wide in Silverlight.  I'll quickly show you how easy it is to do so.

Let's say we want a global property that stores when the Silverlight application was first started.  We will acquire the date/time on application startup and store its value in our application object (App.xaml code behind) via a property.  Here's the code to do so in my App.xaml.vb file:

Private _startDateTime As Date

Public ReadOnly Property StartDateTime() As Date
    Get
        Return _startDateTime
    End Get
End Property

Private Sub Application_Startup(ByVal o As Object, ByVal e As StartupEventArgs) Handles Me.Startup
    _startDateTime = Date.Now
    Me.RootVisual = New Page()
End Sub


When my RootVisual (page) loads, I'll get a handle on the current application object and set a TextBlock's text to the applications startup time which is stored in our application's property we defined.  Here's the code to do so:

Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    Me.tbkDate.Text = DirectCast(Application.Current, App).StartDateTime.ToShortDateString() & " " & DirectCast(Application.Current, App).StartDateTime.ToShortTimeString()
End Sub

In line 2, we get a handle on the current application object by casting "Application.Current" to our "App" type.  Once we have this, we can access our newly created property and set our TextBlock's text property to it.

SilverlightGlobal_Soln.zip (583.88 kb)

Bookmark and Share

GridView Column Sorting - Up/Down Arrows

December 7, 2008 14:32 by wjchristenson2

In this article, I will show you how to manually sort the GridView WebControl and display sort direction arrows.  The GridView has built-in sorting capabilities, however if we want visual feedback as to what column is being sorted and to what direction, we have to perform this ourselves.  While extending the GridView WebControl would be optimal, I'm going to show a quick way to get it done without creating a new GridView control.  Maybe in a future post I'll show how we can create a custom GridView control with sort arrows.  Here is a picture of what our final sorted GridView will look like.

Here is the HTML markup of our GridView:

<asp:GridView 
  ID="GridView1" 
  runat="server" 
  AutoGenerateColumns="False" 
  DataKeyNames="CustomerID" 
  CssClass="gridview" 
  RowStyle-CssClass="gridview_itm" 
  AlternatingRowStyle-CssClass="gridview_aitm" 
  HeaderStyle-CssClass="gridview_hdr" 
  PagerStyle-CssClass="gridview_pgr">
  <Columns>
    <asp:TemplateField>
      <HeaderTemplate>
        <asp:LinkButton ID="CustomerID_SortLnkBtn" runat="server" Text="Customer ID:" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="CustomerID" CausesValidation="false" />
        <asp:ImageButton ID="CustomerID_SortImgBtn" runat="server" Visible="false" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="CustomerID" CausesValidation="false" />
      </HeaderTemplate>
      <ItemTemplate><%#Eval("CustomerID")%></ItemTemplate>
    </asp:TemplateField>
                
    <asp:TemplateField>
      <HeaderTemplate>
        <asp:LinkButton ID="CompanyName_SortLnkBtn" runat="server" Text="Company Name:" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="CompanyName" CausesValidation="false" />
        <asp:ImageButton ID="CompanyName_SortImgBtn" runat="server" Visible="false" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="CompanyName" CausesValidation="false" />
      </HeaderTemplate>
      <ItemTemplate><%#Eval("CompanyName")%></ItemTemplate>
    </asp:TemplateField>
                
    <asp:TemplateField>
      <HeaderTemplate>
        <asp:LinkButton ID="ContactName_SortLnkBtn" runat="server" Text="Contact Name:" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="ContactName" CausesValidation="false" />
        <asp:ImageButton ID="ContactName_SortImgBtn" runat="server" Visible="false" ToolTip="Click to Sort Column" CommandName="Sort" CommandArgument="ContactName" CausesValidation="false" />
      </HeaderTemplate>
      <ItemTemplate><%#Eval("ContactName")%></ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>

The first step is to acquire customers from the Northwind database in the form of a DataTable.  We will then acquire a DataView object from our DataTable, and sort the view.  Once the data in our DataView has been sorted, we will then bind the GridView to the sorted DataView.  To accomplish the data retrieval, sorting, and data binding, I've created the following method:

Private Sub GridView1_DataBind()
    Dim dt As DataTable = New DataTable()

    'fill our datatable w/ customers from the DB
    Using conn As New SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
        Dim sql As String = "SELECT [CustomerID], [CompanyName], [ContactName] FROM [Customers] WITH (NOLOCK)"
        Dim cmd As SqlCommand = New SqlCommand(sql, conn)
        Dim reader As SqlDataReader = Nothing

        Try
            conn.Open()
            reader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
            dt.Load(reader)
        Finally
            If Not reader Is Nothing AndAlso Not reader.IsClosed Then
                reader.Close()
            End If
        End Try
    End Using

    If dt.Rows.Count > 0 Then
        'get a dataView object from our dataTable of customers
        Dim dv As DataView = dt.DefaultView

        'if the user has elected to sort the gridview
        If Not String.IsNullOrEmpty(Me.SortBy("GridView1")) Then
            'get the sort expression and apply it to our dataView
            Dim sortExpr As String = Me.SortBy("GridView1") & " " & IIf(Me.SortDirection("GridView1") = WebControls.SortDirection.Ascending, "ASC", "DESC").ToString()
            dv.Sort = sortExpr
        End If

        'bind the dataView to our GridView
        Me.GridView1.DataSource = dv
        Me.GridView1.DataBind()
    End If
End Sub

The logic is pretty straight forward.  Take note to line 28.  I am using 2 properties to store what column I am sorting by and what direction it is being sorted.  I persist the values in the ViewState and I also pass what GridView I either want to retrieve or store values for.  This allows me to have more that 1 sorting GridView on my page at a time using the same 2 properties.  Here's the code for the 2 properties to assist us with sorting.

''' <summary>
''' Gets or sets the column name to be sorted.
''' </summary>
''' <param name="GridViewID">The unique ID of the GridView.</param>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private Property SortBy(ByVal GridViewID As String) As String
    Get
        Dim o As Object = ViewState(GridViewID & "_SortBy")
        If Not o Is Nothing Then
            Return o.ToString()
        Else
            Return String.Empty
        End If
    End Get
    Set(ByVal value As String)
        ViewState(GridViewID & "_SortBy") = value
    End Set
End Property

''' <summary>
''' Gets or sets the sort direction.
''' </summary>
''' <param name="GridViewID">The unique ID of the GridView.</param>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private Property SortDirection(ByVal GridViewID As String) As SortDirection
    Get
        Dim o As Object = ViewState(GridViewID & "_SortDirection")
        If Not o Is Nothing Then
            Return DirectCast(o, SortDirection)
        Else
            Return WebControls.SortDirection.Ascending
        End If
    End Get
    Set(ByVal value As SortDirection)
        ViewState(GridViewID & "_SortDirection") = value
    End Set
End Property

We have the sort by and sort direction properties (storing/persistance mechanisms).  We have the data retrieval, sorting of the data, and data binding method in place.  Now what we have to do is think about what events we need to account for.  First, on initial page load we'll want to fetch customer data and bind it to our GridView.  We'll only want to bind our GridView the first time the page loads and not subsequent postbacks.

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    If Not Page.IsPostBack Then
        'bind the gridview on page first load
        GridView1_DataBind()
    End If
End Sub

Now we are ready to make the magic happen.  We want to handle the GridView's RowDataBound event and either show or hide our up/down arrows if the user has elected to sort a column.

Private Sub GridView1_RowDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles GridView1.RowDataBound
    'if the row being dataBound is the header row - toggle sort image visibility/directions
    If e.Row.RowType = DataControlRowType.Header Then
        ToggleSortArrows(e.Row, "GridView1")
    End If
End Sub

Private Sub ToggleSortArrows(ByVal headerRow As GridViewRow, ByVal gridViewID As String)
    Dim sortImgBtn As ImageButton = Nothing

    'loop through each cell in the header row
    For Each tc As TableCell In headerRow.Cells
        'loop through each control in the cell
        For Each c As Control In tc.Controls
            'if the control is an image button and is our sort image button
            If TypeOf c Is ImageButton AndAlso c.ID.EndsWith("SortImgBtn") Then
                sortImgBtn = DirectCast(c, ImageButton)

                'if the image button is in the column being sorted
                If Me.SortBy(gridViewID) = sortImgBtn.ID.Split(CChar("_"))(0) Then
                    'show the image button and set its image url (sorted column)
                    sortImgBtn.Visible = True
                    If Me.SortDirection(gridViewID) = WebControls.SortDirection.Ascending Then
                        sortImgBtn.ImageUrl = "~/img/uparrow.gif"
                    Else
                        sortImgBtn.ImageUrl = "~/img/dnarrow.gif"
                    End If
                Else
                    'hide the image button (not a sorted column)
                    sortImgBtn.Visible = False
                End If
            End If
        Next
    Next
End Sub

Basically what we are doing is detecting if the row being DataBound is the header row or not.  If it is, we want to loop through each cell in the header row and get a handle on the column's associated sort image.  We use the ID of the sort image to acquire what column it represents and compare it to our SortBy property.  If it matches, then we want to show the appropriate sort direction image.  We hide the other sort images in non-sorted columns.

The only task we have left to account for is how to fire our sorting event.  Take a quick look at our GridView HTML markup.  The header row has both a LinkButton and ImageButton that raise a GridView command event to which we pass the column name that the user wants to sort by.  We then handle the event in our code behind.

Private Sub GridView1_RowCommand(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewCommandEventArgs) Handles GridView1.RowCommand
    If e.CommandName.ToUpper = "SORT" Then
        InitializeSort(e.CommandArgument, "GridView1")
        GridView1_DataBind()
    End If
End Sub

Private Sub InitializeSort(ByVal sortBy As String, ByVal gridViewID As String)
    If Me.SortBy(gridViewID) = sortBy Then
        If Me.SortDirection(gridViewID) = WebControls.SortDirection.Ascending Then
            Me.SortDirection(gridViewID) = WebControls.SortDirection.Descending
        Else
            Me.SortDirection(gridViewID) = WebControls.SortDirection.Ascending
        End If
    Else
        Me.SortBy(gridViewID) = sortBy
        Me.SortDirection(gridViewID) = WebControls.SortDirection.Ascending
    End If
End Sub

Once we capture our sort row command, we initialize our SortBy and SortDirection properties.  We either toggle the direction of the sorted column or the sorted column is a new column to be sorted to which we default the column to be sorted Ascending.

I hope this article helped a bit.  It's a quick way to get a GridView sorted with visual indicators (sort arrows) without creating a new custom GridView control.

GridViewSorting_Soln.zip (90.61 kb)

Bookmark and Share

Silverlight Performance Tips

November 19, 2008 04:30 by wjchristenson2

1)  Do set IsWindowless=False.  There is a high performance price in rendering windowless controls.  However I've found that if you do this, you might run into some problems.  First, if you want your background of your SL application to be transparent, then you must set IsWindowless to True.  Also, if you have HTML overlays (modals) over your SL application, you'll also need to set IsWindowless = True.

2)  Do NOT set the Silverlight's HTML control background property to transparent or any variation thereof (make it opaque).   If the background property is set to such, each render call will go through a blending sequence which adds to a higher CPU cost.

3)  Do change the Silverlight's application default MaxFrameRate.  The default value is 60.  Most SL applications will look/run fine anywhere between 15 to 30.  You can change the MaxFrameRate programmatically or simply markup the Silverlight HTML control.

4)  Do NOT do text size animations.  When you animate the size of text in SL, it uses hinting to smooth each text glyph.  When animating text size, SL may drop frames due to this.  If you can, use a vector graphic to represent large text animations.

5)  Do use Visibility instead of Opacity whenever possible.  Even if an object's opacity is set to 0, SL will still account for it and its still technically rendered.  Setting the object's visibility to Collapsed will cause SL to ignore rendering the object.

6)  When using the MediaElement object, do not specify its Width and Height.  Let SL render the object at its natural size.

7)  Do not set the Width and Height on path objects.  Rely on the points defined for the path.

8)  When displaying a double's value, do use Double.ToString(CultureInfo.InvariantCulture) rather than Double.ToString().  This will alleviate the need for SL to acquire the culture setting before displaying the double and CurltureInfo.InvariantCulture is optimized for perormance.

9)  If your application is very large, consider loading pieces of it "on demand".

Bookmark and Share

Save Scroll Position GridView Control

October 29, 2008 11:09 by wjchristenson2

I've seen a lot of articles on the web describing how to scroll a GridView.  Some of those articles may even go a little deeper and show you how to persist the scroll position across PostBacks.  What I haven't been able to find is how to encapsulate the scrolling and saving/persisting of the scroll position into a custom GridView control.  That is what I am going to show you how to do today.

There are many ways a developer may want to scroll their GridView (horizontal, vertical, freezing the header/footer, etc).  I'm not going to go into creating a bullet proof GridView control to accomplish those tasks.  My objective is to show you a technique to save/set the scroll position across PostBacks within a custom GridView control.

Here is what our end GridView control will look like.  First you can see a scrollable GridView.  When we scroll the GridView and then click the PostBack button, the page will perform a PostBack and the <div> will scroll itself back to where we left off.



So let's get to it.  First thing we do is create a new control and inherit from the GridView.  We then implement the IPostBackDataHandler to get and set our scroll position across PostBacks.  You'll see the LoadPostData function accomplishes this.  Take note that in line 3 I am referencing the name of my hidden field that I'll go over later.  We also tell the page that our new custom GridView (Me) has data to PostBack and we can do this in the GridView's Init.

Public Function LoadPostData(ByVal postDataKey As String, ByVal postCollection As System.Collections.Specialized.NameValueCollection) As Boolean Implements System.Web.UI.IPostBackDataHandler.LoadPostData
    'get and set the new scroll position
    Dim postedValue As String = postCollection(Me.ClientID & "_scroll")
    If Not postedValue Is Nothing Then
        Dim presentValue As Integer = Me.ScrollPosition
        Me.ScrollPosition = CType(postedValue, Integer)
        Return Not presentValue.Equals(Me.ScrollPosition)
    End If
End Function

Public Sub RaisePostDataChangedEvent() Implements System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent
    'do nothing
End Sub

Private Sub ScrollingGridView_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
    If Not Me.Page Is Nothing Then
        'indicates that the control has data to postback
        Me.Page.RegisterRequiresPostBack(Me)
    End If
End Sub

In order to scroll my new custom GridView, I need to wrap my GridView with a <div> tag.  To do this, I can override the Render method like so:

Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
    If Not Me.Page Is Nothing Then
        Me.Page.VerifyRenderingInServerForm(Me)
    End If

    Me.PrepareControlHierarchy()

    If Not Me.DesignMode Then
        If String.IsNullOrEmpty(Me.ClientID) Then
            Throw New HttpException("ScrollingGridView must be parented!")
        End If

        writer.AddAttribute(HtmlTextWriterAttribute.Id, String.Format("{0}_div", Me.ClientID), True)
        writer.AddAttribute("onScroll", "saveScrollPos('" & String.Format("{0}_scroll", ClientID) & "', '" & String.Format("{0}_div", Me.ClientID) & "');")
        writer.AddStyleAttribute(HtmlTextWriterStyle.OverflowY, "auto")
        writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "300px")
        writer.RenderBeginTag(HtmlTextWriterTag.Div)
    End If

    Me.RenderContents(writer)

    If Not Me.DesignMode Then
        writer.RenderEndTag()
    End If
End Sub

Notice that when I render my <div> that I also wire in a call to my JavaScript function to save the scroll position "onScroll".  At this point I have completed the PostBack data handling and I have shown you how to wrap a <div> around our GridView so we can scroll and it will call a JavaScript function "onScroll" that will save the position to a hidden field.

The next phase is to emit our JavaScripts.  I always emit JavaScript in the PreRender phase of a control's lifecycle as the ClientID should be set at that point.  We need a function to set the scroll position and a function to save the scroll position.  Here they are:

'generate & register javascript blocks
Dim key As String = "ScrollingGridView"
Dim script As StringBuilder = New StringBuilder()
With script
    .AppendLine("function saveScrollPos(whereID, whatID) {")
    .AppendLine("  document.getElementById(whereID).value = document.getElementById(whatID).scrollTop;")
    .AppendLine("}")
    .AppendLine("function setScrollPos(whereID, whatID) {")
    .AppendLine("  document.getElementById(whatID).scrollTop = (document.getElementById(whereID).value.length > 0) ? document.getElementById(whereID).value : 0;")
    .AppendLine("}")
End With

If Not sm Is Nothing Then
    ScriptManager.RegisterStartupScript(Me.Page, Me.GetType(), key, script.ToString(), True)
Else
    Me.Page.ClientScript.RegisterStartupScript(Me.GetType(), key, script.ToString(), True)
End If

Notice we either acquire or save the scroll position to our hidden field.  Now how do we emit a hidden field inside our GridView?  My first attempts were to override the Render of the GridView control and emit hidden field there.  It didn't work.  I ended up registering the hidden field via .NET's scripting objects instead.  Check it out:

'register our hidden field to save our scroll position
If Not sm Is Nothing Then
    ScriptManager.RegisterHiddenField(Me.Page, String.Format("{0}_scroll", Me.ClientID), Me.ScrollPosition.ToString())
Else
    Me.Page.ClientScript.RegisterHiddenField(String.Format("{0}_scroll", Me.ClientID), Me.ScrollPosition.ToString())
End If

Notice that I assign the scroll position to the hidden field's value.  Last thing we need to do is register a startup script to get the persisted scroll position from our hidden field and scroll our <div>.  If you remember we created a JavaScript function for this (setScrollPosition).

'generate & register startup javascripts
key = String.Format("{0}_setScrollPos", Me.ClientID)
script = New StringBuilder()
script.AppendLine("setScrollPos('" & String.Format("{0}_scroll", Me.ClientID) & "','" & String.Format("{0}_div", Me.ClientID) & "');")
If Not sm Is Nothing Then
    ScriptManager.RegisterStartupScript(Me.Page, Me.GetType(), key, script.ToString(), True)
Else
    Me.Page.ClientScript.RegisterStartupScript(Me.GetType(), key, script.ToString(), True)
End If

Putting it all Together:

In summary, we persist the scroll position of the div that wraps our GridView via a hidden field which is registered via the Page.ClientScript or ScriptManager object.  We wrap the GridView with a <div> by overriding the GridView's Render method.  The <div> saves the scroll position to the hidden field as the <div> is scrolled.  We acquire the value stored in the hidden field via the LoadPostData function.  Once a PostBack occurs, we set the scroll position of the <div> with a startup script.


ScrollingGridView_Soln.zip (97.19 kb)
Bookmark and Share