Thursday, October 9, 2008

Silverlight 2.0 Interacting with html

With SilverLight 2.0 you can interact and handle events with the html elements on your page.  Here is a simple example that places a select (drop down control) on a web page which will change the color of a ellipse on a SilverLight app.  So lets start with the html

?
<body style="height: 100%; margin: 0;">
    <form id="form1" runat="server" style="height: 100%;">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <br />
    <span>Select a Color </span>
    <select id="ddColor">
        <option>Red</option>
        <option>Blue</option>
        <option>Green</option>
    </select>
    <br />
    <br />
    <div style="height: 100%;">
        <asp:Silverlight ID="Xaml1" runat="server" Source="~/ClientBin/HtmlAndSilverlight.xap"
            MinimumVersion="2.0.30923.0" Width="100%" Height="100%" />
    </div>
    </form>
</body>


Now the XAML

?
<UserControl x:Class="HtmlAndSilverlight.Page"
    Width="400" Height="300">
    <Canvas x:Name="LayoutRoot" Background="Tan" >
        <Ellipse x:Name="el" Width="400" Height="300" Fill="Red"></Ellipse>
    </Canvas>
</UserControl>

Now build the app so we have intellisense for the controls.  Ok first off lets get access to the drop down (select) on the web page. Then we can use AttachEvent to handle the onchange event.   

?
Dim cbo As HtmlElement
Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    cbo = HtmlPage.Document.GetElementById("ddColor")
    cbo.AttachEvent("onchange", AddressOf ColorChanged)
End Sub

Once the user selects a color we will change the color of the ellipse.  Here is the complete code listing

?
Imports System.Windows.Browser
 
Partial Public Class Page
    Inherits UserControl
 
    Public Sub New()
        InitializeComponent()
    End Sub
    Dim cbo As HtmlElement
    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        cbo = HtmlPage.Document.GetElementById("ddColor")
        cbo.AttachEvent("onchange", AddressOf ColorChanged)
    End Sub
 
    Private Sub ColorChanged(ByVal sender As Object, ByVal e As HtmlEventArgs)
        Dim x = CInt(cbo.GetAttribute("selectedIndex").ToString)
        Select Case x
            Case 0
                el.Fill = New SolidColorBrush(Colors.Red)
            Case 1
                el.Fill = New SolidColorBrush(Colors.Blue)
            Case 2
                el.Fill = New SolidColorBrush(Colors.Green)
        End Select
    End Sub
End Class


Hope this helps

Sunday, October 5, 2008

Silverlight 2.0 Create a context menu

Silverlight 2 does not come with a context menu control.  You could always handle the html document's oncontextmenu event and open a popcontrol to use as a context menu.  This sample should help you get started.

<UserControl x:Class="SilverlightContextMenu.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid>
        <Rectangle x:Name="LayoutRoot" Fill ="Green" ></Rectangle >
        <Popup x:Name="menu">
            <StackPanel>
                <Button x:Name="btnRed" Content="Red"></Button>
                <Button x:Name="btnWhite" Content="White"></Button>
                <Button x:Name="btnBlue" Content="Blue"></Button>
            </StackPanel>
        </Popup>
    </Grid>
</UserControl>
Imports System.Windows.Browser

Partial Public Class Page
    Inherits UserControl

    Dim WithEvents cm As ContextMenuCatcher

    Public Sub New()
        InitializeComponent()
    End Sub

    Private Sub btnBlue_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnBlue.Click
        LayoutRoot.fill = New SolidColorBrush(Colors.Blue)
        menu.IsOpen = False
    End Sub

    Private Sub btnRed_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnRed.Click
        LayoutRoot.fill = New SolidColorBrush(Colors.Red)
        menu.IsOpen = False
    End Sub

    Private Sub btnWhite_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnWhite.Click
        LayoutRoot.fill = New SolidColorBrush(Colors.White)
        menu.IsOpen = False
    End Sub

    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        cm = New ContextMenuCatcher(LayoutRoot)
        menu.IsOpen = False
    End Sub

    Private Sub cm_RightClick(ByVal sender As Object, ByVal e As RightClickEventArgs) Handles cm.RightClick
        menu.HorizontalOffset = e.AbsolutePoint.X
        menu.VerticalOffset = e.AbsolutePoint.Y
        menu.IsOpen = True
    End Sub

    Private Sub menu_MouseLeave(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles menu.MouseLeave
        menu.IsOpen = False
    End Sub

End Class

Public Class ContextMenuCatcher
    Public Event RightClick(ByVal sender As Object, ByVal e As RightClickEventArgs)
    Dim ctrl As UIElement

    Public Sub New(ByVal c As UIElement)
        ctrl = c

        HtmlPage.Document.AttachEvent("oncontextmenu", AddressOf OnContextMenu)
    End Sub

    Private Sub OnContextMenu(ByVal sender As Object, ByVal e As HtmlEventArgs)
        System.Diagnostics.Debug.WriteLine(e.OffsetX.ToString)
        System.Diagnostics.Debug.WriteLine(e.OffsetY.ToString)
        Dim pt As New Point(e.OffsetX, e.OffsetY)
        e.PreventDefault()
        e.StopPropagation()
        RaiseEvent RightClick(Me, New RightClickEventArgs(ctrl, pt))
    End Sub

End Class

Public Class RightClickEventArgs

    Dim m_Source As UIElement
    Public Property Source() As UIElement
        Get
            Return m_Source
        End Get
        Set(ByVal value As UIElement)
            m_Source = value
        End Set
    End Property

    Dim m_RelativePoint As Point
    Public Property RelativePoint() As Point
        Get
            Return m_RelativePoint
        End Get
        Set(ByVal value As Point)
            m_RelativePoint = value
        End Set
    End Property

    Dim m_AbsolutePoint As Point
    Public Property AbsolutePoint() As Point
        Get
            Return m_AbsolutePoint
        End Get
        Set(ByVal value As Point)
            m_AbsolutePoint = value
        End Set
    End Property

    Friend Sub New(ByVal source As UIElement, ByVal absolutePoint As Point)
        Me.Source = source
        Me.AbsolutePoint = absolutePoint
        Me.RelativePoint = GetPosition(source)
    End Sub

    Public Function GetPosition(ByVal relativeTo As UIElement) As Point
        Dim transform As GeneralTransform = Application.Current.RootVisual.TransformToVisual(relativeTo)
        Return transform.Transform(AbsolutePoint)
    End Function
End Class
 
You also have to make the silverlight control windowless for this to work
<asp:Silverlight ID="Xaml1" runat="server" Source="~/ClientBin/SilverlightContextMenu.xap" MinimumVersion="2.0.30923.0" Width="100%" Height="100%" Windowless="true" />

Silverlight 2 RC0 DataGrid CommittingEdit work around

In Silverlight 2 Beta 2 DataGrid had a CommittingEdit event which was a great event to update the data in an ado.net dataservice.   Unfortunately this event was removed in the RC0 of the datagrid.  As a work around Jonathan Shen suggested using a template column and using the LostFocus event to update your dataservice data.


                 <data:DataGridTemplateColumn Header="Command">
                        <data:DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding FirstName}"></TextBlock>
                            </DataTemplate>
                        </data:DataGridTemplateColumn.CellTemplate>
                        <data:DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <TextBox Text="{Binding FirstName}"LostFocus="TextBox_LostFocus"></TextBox>   //you can detect other events.
                            </DataTemplate>
                        </data:DataGridTemplateColumn.CellEditingTemplate>
                    </data:DataGridTemplateColumn>

Well this works fine but I don't want to have to define all my columns this way.  Sometimes it is nice to just let the datagrid autogenerate the columns. So I decided to use the DataGrid's PreparingCellForEdit to add the handler for the LostFocus event.  Couple of other things you need to do.  First Remove the old event handler so the event does not fire multiple times.  Second we need a variable to keep track of the item that was edited when the lost focus event is fired we will be on another record.

Dim prod As Northwind.Products
Private Sub dgProducts_PreparingCellForEdit(ByVal sender As Object, ByVal e As System.Windows.Controls.DataGridPreparingCellForEditEventArgs) Handles dgProducts.PreparingCellForEdit
    RemoveHandler e.EditingElement.LostFocus, AddressOf Textbox_LostFocus
    AddHandler e.EditingElement.LostFocus, AddressOf Textbox_LostFocus
    prod = DirectCast(dgProducts.SelectedItem, Northwind.Products)
End Sub
Friend Sub Textbox_LostFocus(ByVal sender As Object, ByVal e As EventArgs)
    proxy.UpdateObject(prod)
End Sub

Here is the complete code

Imports System.Collections.ObjectModel
Imports System.Data.Services.Client
Imports System.Diagnostics
Partial Public Class Page
    Inherits UserControl
    Public Sub New()
        InitializeComponent()
    End Sub
    Dim q As DataServiceQuery(Of Northwind.Products)
    Dim proxy As Northwind.NorthwindEntities
    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        Dim address = New Uri(Application.Current.Host.Source, "../WebDataService1.svc")
        proxy = New Northwind.NorthwindEntities(address)
        Dim qry = From p In proxy.Products Select p
        q = CType(qry, Global.System.Data.Services.Client.DataServiceQuery(Of Northwind.Products))
        q.BeginExecute(AddressOf ProductsLoaded, Nothing)
    End Sub
    Friend Sub ProductsLoaded(ByVal ar As System.IAsyncResult)
        Dim c = q.EndExecute(ar)
        Dim d As New ObservableCollection(Of Northwind.Products)
        For Each p In c
            d.Add(p)
        Next
        Dim GetOnRightThread As New SetTheDataSource(AddressOf SetDataSource)
        Dispatcher.BeginInvoke(GetOnRightThread, New Object() {d})
    End Sub
    Delegate Sub SetTheDataSource(ByVal d As ObservableCollection(Of Northwind.Products))
    Friend Sub SetDataSource(ByVal d As ObservableCollection(Of Northwind.Products))
        dgProducts.ItemsSource = d
    End Sub
    Dim prod As Northwind.Products
    Private Sub dgProducts_PreparingCellForEdit(ByVal sender As Object, ByVal e As System.Windows.Controls.DataGridPreparingCellForEditEventArgs) Handles dgProducts.PreparingCellForEdit
        RemoveHandler e.EditingElement.LostFocus, AddressOf Textbox_LostFocus
        AddHandler e.EditingElement.LostFocus, AddressOf Textbox_LostFocus
        prod = DirectCast(dgProducts.SelectedItem, Northwind.Products)
    End Sub
    Friend Sub Textbox_LostFocus(ByVal sender As Object, ByVal e As EventArgs)
        proxy.UpdateObject(prod)
    End Sub
    Friend Sub SaveComplete(ByVal ar As System.IAsyncResult)
        proxy.EndSaveChanges(ar)
    End Sub
    Private Sub btnSave_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnSave.Click
        proxy.BeginSaveChanges(AddressOf SaveComplete, Nothing)
    End Sub
End Class

Monday, August 18, 2008

Asp.Net Routing

New to the .Net Framework 3.5 SP 1 is the System.Web.Routing namespace.  The classes in the routing namespace allow you to use urls that do not map to a web page.


For this example I created a new web application.  To start off with lets add a reference to the system.web.routing and system.web.abstractions.  Open up the web.config file and lets add the UrlRoutingModule to the httpmodules section

        <httpModules>
            <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </httpModules>

Next we need to add a WebFormRouteHandler class which will be our route handler.

Imports System.Web.Routing
Imports System.Web.Compilation
Public Class WebFormRouteHandler
    Implements IRouteHandler
    Private _Path As String
    Public Property Path() As String
        Get
            Return _Path
        End Get
        Set(ByVal value As String)
            _Path = value
        End Set
    End Property
    Public Sub New(ByVal p As String)
        Path = p
    End Sub
    Public Function GetHttpHandler(ByVal requestContext As System.Web.Routing.RequestContext) As System.Web.IHttpHandler Implements System.Web.Routing.IRouteHandler.GetHttpHandler
        For Each value In requestContext.RouteData.Values
            requestContext.HttpContext.Items(value.Key) = value.Value
        Next
        If Not String.IsNullOrEmpty(Path) Then
            Return TryCast(BuildManager.CreateInstanceFromVirtualPath(Path, GetType(Page)), IHttpHandler)
        Else
            Return Nothing
        End If
    End Function
End Class

Finally we have to add global.asax so we can start the routing when the web site starts up


Imports System.Web.SessionState
Imports System.Web.Routing
Public Class Global_asax
    Inherits System.Web.HttpApplication
    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when the application is started.
        Dim userHandler As New WebFormRouteHandler("~/user.aspx")
        With RouteTable.Routes
            ' pattern of the url to match
            .Add(New Route("user/{user}", userHandler))
        End With
    End Sub
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when the session is started
    End Sub
    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires at the beginning of each request
    End Sub
    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires upon attempting to authenticate the use
    End Sub
    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when an error occurs
    End Sub
    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when the session ends
    End Sub
    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when the application ends
    End Sub
End Class

Finally we need 2 web pages

Default.aspx

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Default.aspx.vb" Inherits="RoutingTest._Default" %>
<!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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <h1>ASP.NET System.Web.Routing with WebForms sample</h1>
    <div><a href="user/ken">User test</a></div>
    </div>
    </form>
</body>
</html>

user.aspx
<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="User.aspx.vb" Inherits="RoutingTest.User" %>
<!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></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
            <h1>
            Users</h1>
        <div>
            User:
            <%=Context.Items("user")%></div>
        <hr />
        <div>
            Change the user in the URL.</div>
        <div>
            The WebFormRouteHandler adds the matched values to the HTTPContext.Items collection
            (exists for lifetime of request only).</div>
    </div>
    </form>
</body>
</html>

Hope this helps

Saturday, July 5, 2008

Publishing a VB Silverlight 2 Beta 2 app which uses a WCF service

Note this works with the released version of Silverlight 2

I created a Silverlight 2 beta 2 app which uses a WCF Silverlight service which worked fine localy but did call the service when I published it to my web host.  After playing around with the different settings I finally came across an entry in the Silverlight Forums by sladapter with a solution. 


So lets create a simple Silverlight 2 App to demo how to do this.  I created a silverlight app with a web application project.   I prefer web applications to web sites but a web site will work the same.  

Add a WCF Silverlight- enabled service named  service1 to the web application. 

This is the code I am using for the service

Imports System.ServiceModel
Imports System.ServiceModel.Activation
<ServiceContract(Namespace:="")> _
<AspNetCompatibilityRequirements(RequirementsMode:=AspNetCompatibilityRequirementsMode.Allowed)> _
Public Class Service1
    <OperationContract()> _
    Public Function SayHello() As String
        ' Add your operation implementation here
        Return "Hello World"
    End Function
End Class

Lets add a TextBlock to the Page.xaml to display our message.

<UserControl x:Class="SilverlightApplication2.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
            <TextBlock x:Name="txt">Loading..</TextBlock>
    </Grid>
</UserControl>

Now run the app.  Once that is done we can add a service reference to our silverlight app.  Press the arrow on the Discover button and select services in the solution.  You should windup with something like this.


In the silverlight app open up the file ServiceReferences.ClientConfig

<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_Service1" maxBufferSize="65536"
                    maxReceivedMessageSize="65536">
                    <security mode="None" />
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:1205/Service1.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_Service1" contract="ServiceReference1.Service1"
                name="BasicHttpBinding_Service1" />
        </client>
    </system.serviceModel>
</configuration>

In the endpoint address change the contract to include the project name.

<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_Service1" maxBufferSize="65536"
                    maxReceivedMessageSize="65536">
                    <security mode="None" />
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:1205/Service1.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_Service1" contract="SilverlightApplication2.ServiceReference1.Service1"
                name="BasicHttpBinding_Service1" />
        </client>
    </system.serviceModel>
</configuration>

Now lets add some code to call the service. Page.Xaml.VB

Partial Public Class Page
    Inherits UserControl
    Dim current As String
    Public Sub New()
        InitializeComponent()
    End Sub
    Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        Dim ws As New ServiceReference1.Service1Client
        AddHandler ws.SayHelloCompleted, AddressOf HelloComplete
        ws.SayHelloAsync()
    End Sub
    Private Sub HelloComplete(ByVal sender As Object, ByVal e As ServiceReference1.SayHelloCompletedEventArgs)
        txt.Text = e.Result
    End Sub
End Class

Now if we run the app you should see Hello World but when published you will only see loading.   So lets change how we create the service so that this will work once deployed.  Basically we tell the service to use the current web address.

Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    Dim address = New Uri(Application.Current.Host.Source, "../Service1.svc")
    Dim ws As New ServiceReference1.Service1Client("BasicHttpBinding_Service1", address.AbsoluteUri)
    AddHandler ws.SayHelloCompleted, AddressOf HelloComplete
    ws.SayHelloAsync()
End Sub

Hope this helps