Friday, June 19, 2009

Code Browser Take 2: Silverlight

After publishing an AJAX-based source code browser last week, I decided to do an iteration with Silverlight support. Our company is exploring this technology, so it was the perfect "proof of concept" project to get familiar with Silverlight before wiring it in at the office.

Download the Source Code

This post will walk you through the steps I took to go from a project that had no Silverlight whatsoever to the existing project available here.

My first step was to do some basic re-factoring of the layout. If you recall, the first version used a base page to wire in the JavaScript and style sheets. Because I now have two pages, I decided to pull that functionality into a MasterPage so that I could have a common header/footer/etc. This was fairly straightforward:

  1. Create a master page with the common elements, including the script manager and outer form
  2. Move the logic from the base page to the master page code behind
  3. Rename Default.aspx to Default_.aspx and then add a new Default.aspx with the MasterPage reference
  4. Move over the pertinent items, and test

My first round of testing found that my styling broke because the CSS was looking for a #tree to override the anchor tag behavior. Because this was a span running on the server so the controller could render into it, I moved the span element up and used a placeholder to render the control instead. Note the span has a "local" id whereas the placeholder has a server id that basically takes on the id of the master page in front of it. It looks like this in the .aspx:

<span id="_tree">
   <asp:PlaceHolder ID="_tree" runat="server"/>
</span>

But gets rendered like this:

<span id="_tree">
 <div id="ctl00__contentPlaceHolder1_ctl00__pnlTree">
 ... etc etc

The next step was to prepare the models for consumption by a service. Silverlight runs in the browser and therefore cannot directly access any of the existing assemblies (it actually has its own, stripped-down version of the CLR). This becomes true "client-server". While we used callbacks for the AJAX version, this would use a true service. Because my main web project was using interfaces, I needed something concrete to serialize across the wire. I created a folder called Transport and began building my objects.

The first change I had to make was with the parent directory. The original models had an IDirectory reference, which would obviously need to be concrete for a service. Because I really don't need the parent directory, I decided to split it from the original interface and create a new IParentDirectory interface instead. Both the FileModel and the DirectoryModel then simply implement IFile, IParentDirectory and IDirectory, IParentDirectory respectively.

FileTransport was first. I could implement IFile for this and keep it fairly much "as is." To ease converting from my interface type to a concrete instance, I allowed the constructor to take in an IFile and set its properties. I also added a flag on the constructor to determine whether or not to load the content. On building the tree, I will not send content across the wire, only when requesting the actual source. The completed class looks like this:

using System;
using Interface.Model;

namespace CodeBrowser.Transport
{
    /// <summary>
    ///     File transport
    /// </summary>
    [Serializable]
    public class FileTransport : IFile 
    {
        /// <summary>
        ///     Required constructor for serialization
        /// </summary>
        public FileTransport()
        {
            
        }

        /// <summary>
        ///     Constructor - no content
        /// </summary>
        /// <param name="file">A file to transport</param>
        public FileTransport(IFile file) : this(file, false)
        {
            
        }

        /// <summary>
        ///     Constructor with content
        /// </summary>
        /// <param name="file">A file to transport</param>
        /// <param name="loadContent">True if content should be loaded as well</param>
        public FileTransport(IFile file, bool loadContent) 
        {
            Name = file.Name;
            Path = file.Path;
            Extension = file.Extension;
            Size = file.Size;
            Content = loadContent ? file.Content : new byte[0];
        }

        /// <summary>
        ///     Name of the node
        /// </summary>
        public string Name { get; set; }
        
        /// <summary>
        ///     Path to the node
        /// </summary>
        public string Path { get; set;}
        
        /// <summary>
        ///     Extension for the file
        /// </summary>
        public string Extension { get; set; }
        
        /// <summary>
        ///     Content of the file
        /// </summary>
        public byte[] Content { get; set; }
        
        /// <summary>
        ///     Size of the file
        /// </summary>
        public int Size { get; set; }                       
    }
}

The DirectoryTransport was a little more interesting. I needed to send concrete directories and concrete files, so using the IList defined in the interface was not possible. I settled for implementing IFileSystemNode (just a name and a path) and then created a distinct List<FileTransport> and List<DirectoryTransport> instead. The constructor also would take an IDirectory and then recurse to build out the transport structure. The finished class:

using System;
using System.Collections.Generic;
using Interface.Model;

namespace CodeBrowser.Transport
{
    /// <summary>
    ///     Directory transport
    /// </summary>
    [Serializable]
    public class DirectoryTransport : IFileSystemNode
    {
        /// <summary>
        ///     Required constructor for serializable
        /// </summary>
        public DirectoryTransport()
        {
            
        }

        /// <summary>
        ///     Constructor from interface
        /// </summary>
        /// <param name="directory"></param>
        public DirectoryTransport(IDirectory directory)
        {
            Name = directory.Name;
            Path = directory.Path;
            FileContents = new List<FileTransport>();
            DirectoryContents = new List<DirectoryTransport>();
            
            foreach(IFileSystemNode node in directory.Contents)
            {
                if (node is IFile)
                {
                    FileContents.Add(new FileTransport(node as IFile));
                }
                else if (node is IDirectory)
                {
                    DirectoryContents.Add(new DirectoryTransport(node as IDirectory));
                }
            }
        }
        /// <summary>
        ///     Name of the node
        /// </summary>
        public string Name { get; set; }
        
        /// <summary>
        ///     Path to the node
        /// </summary>
        public string Path { get; set; }
                
        /// <summary>
        ///     Files 
        /// </summary>
        public List<FileTransport> FileContents { get; set; }

        /// <summary>
        ///     directories
        /// </summary>
        public List<DirectoryTransport> DirectoryContents { get; set; }
                    
    }
}

The transport objects made wiring in a service very easy. The first service that provides the full tree node (sans code) was as simple as:

[WebMethod]
public DirectoryTransport GetMasterNode()
{
    return new DirectoryTransport(TreeCache.GetMasterNode()); 
}

The same cache is tapped into, the IDirectory fed into the DirectoryTransport and then serialized across the wire (you can see what it looks like locally on your machine by browsing to the service and then invoking it).

The file detail method was also straightforward. Given a path, it iterates the tree structure until it finds the right file (this is not intelligent yet - no hashing or understanding partial paths, etc — that will be a refactor) and then processes the code to send as a string.

Because of converting from bytes to string, I ripped out the translation piece from Leaf and put it into Utility with a switch to encapsulate in a <pre> tag for syntax highlighting (alas, the Silverlight version doesn't highlight or allow selection ... yet).

You can peruse the code to see this. These were the modifications to the main code base. You can see how having a multi-tiered application made it very easy and straightforward to stand up the pieces we needed for a service! No major refactoring or restructuring of objects, just a few tweaks and we were ready to go. Now for the fun part ... the Silverlight piece.

Of course, all of the prerequisites for Silverlight need to be pulled down, installed, and available. I used the latest non-beta version (2.0 as of this writing) and grabbed the toolkit as well. The page I made was very simple and straightforward. My steps to make the Silverlight project:

  1. Added a new project to my solution as a Silverlight application.
  2. When prompted, I chose to embed the test page into an existing web application (CodeBrowser)
  3. In my CodeBrowser application, I created a new page called Silver.aspx and simply added the Silverlight control and reference to the application, then deleted the test pages. My MasterPage has a link to the AJAX and Silverlight versions.
  4. Finally, I added a reference to the DataAccess.asmx service to pull in the tree and file contents

The Xaml was simple - a stacked panel with a tree view and a scroll viewer for the code, with some styling and colors and fonts sprinkled in via Expression Blend. It looks like this:

<UserControl xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"  x:Class="SourceSilver.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="1024" Height="768" BorderThickness="2" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
    <StackPanel Orientation="Horizontal" Width="1000" Height="760" Background="White">
          <controls:TreeView  x:Name="trvTree" Width="250" BorderThickness="0,0,0,0" FontFamily="Arial" FontSize="10" FontWeight="Bold" TabNavigation="Cycle" Cursor="Hand"/>
        <TextBlock Width="10"/>
        <StackPanel Orientation="Vertical">
                <TextBlock Height="10"/>
                <ScrollViewer Width="700" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Height="700" x:Name="svCode" FontFamily="./Fonts/Fonts.zip#Consolas" Cursor="IBeam" Margin="1,1,1,1" d:IsStaticText="True" 
FontSize="10">                                        
                 <ScrollViewer.Background>
                  <LinearGradientBrush EndPoint="0.5,1"
 StartPoint="0.5,0" SpreadMethod="Repeat">
                   <GradientStop Color="#FFF5EEEE"/>
                   <GradientStop Color="#FFCF9494" Offset="1"/>
                  </LinearGradientBrush>
                 </ScrollViewer.Background>                                        
                </ScrollViewer>
            </StackPanel>
    </StackPanel>    
</UserControl>

The code behind was easier than I expected. First, I instantiated my web service agent:

...
readonly DataAccessSoapClient _service = new DataAccessSoapClient();
...

Next, wire in some events for listening to the service and start the service to load the main nodes (done in the constructor):

public Page()
{
    InitializeComponent();
    
    _service.GetMasterNodeCompleted += _ServiceGetMasterNodeCompleted;
    _service.GetFileDetailsCompleted += _ServiceGetFileDetailsCompleted;
    _service.GetMasterNodeAsync();
}

Finally, in the events, I wired in two activities. Grabbing the node starts a process to build nested TreeViewItem that I add to the control. I wire in a click event on the directories so they toggle:

static void _DirectorySelected(object sender, System.Windows.RoutedEventArgs e)
{
    TreeViewItem item = sender as TreeViewItem;
    if (item != null)
    {
        item.IsExpanded = !item.IsExpanded;
    }
}

I recurse the nodes, making more items as needed. Note for the files that I bind a separate click event and make the FileTransport the DataContext for that node:

foreach (FileTransport file in node.FileContents)
{
    TreeViewItem fileItem = new TreeViewItem {DataContext = file, Header = file.Name};
    fileItem.Selected += _FileItemSelected;
    root.Items.Add(fileItem);
}

Down the road I may refactor to use templates but this worked well for now. When the file is selected, we take the data context and cast it back to the file, then call the service to give us the actual code:

FileTransport file = item.DataContext as FileTransport;
if (file != null)
{
    svCode.Content = new TextBlock {Text = "Loading..."}; 
    _service.GetFileDetailsAsync(file.Path);
}

Easiest of all, when the call returns, we simply inject the code into the ScrollView control:

void _ServiceGetFileDetailsCompleted(object sender, GetFileDetailsCompletedEventArgs e)
{
    svCode.Content = e.Result;
}

That's it! I published it and was able to run it. Obviously, there is still a lot of work to do. The AJAX side needs to stop loading the entire tree and instead lazy load as you expand nodes. The Silverlight side needs some highlighting and clean-up of the UI. Hopefully, however, if you are new to Silverlight, this was a useful walk through for taking existing code and wiring it in, and using some of the new controls in Silverlight.

Until next time ...

Jeremy Likness

No comments:

Post a Comment