Click here to Skip to main content
Click here to Skip to main content

Code Browser Take 2: Silverlight

, 19 Jun 2009
Rate this:
Please Sign up or sign in to vote.
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...

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.

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
{
    /// <span class="code-SummaryComment"><summary>
</span>    ///     File transport
    /// <span class="code-SummaryComment"></summary>
</span>    [Serializable]
    public class FileTransport : IFile 
    {
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Required constructor for serialization
        /// <span class="code-SummaryComment"></summary>
</span>        public FileTransport()
        {
            
        }

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

        /// <span class="code-SummaryComment"><summary>
</span>        ///     Constructor with content
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="file">A file to transport</param>
</span>        /// <span class="code-SummaryComment"><param name="loadContent">True if content should be loaded as well</param>
</span>        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];
        }

        /// <span class="code-SummaryComment"><summary>
</span>        ///     Name of the node
        /// <span class="code-SummaryComment"></summary>
</span>        public string Name { get; set; }
        
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Path to the node
        /// <span class="code-SummaryComment"></summary>
</span>        public string Path { get; set;}
        
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Extension for the file
        /// <span class="code-SummaryComment"></summary>
</span>        public string Extension { get; set; }
        
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Content of the file
        /// <span class="code-SummaryComment"></summary>
</span>        public byte[] Content { get; set; }
        
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Size of the file
        /// <span class="code-SummaryComment"></summary>
</span>        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
{
    /// <span class="code-SummaryComment"><summary>
</span>    ///     Directory transport
    /// <span class="code-SummaryComment"></summary>
</span>    [Serializable]
    public class DirectoryTransport : IFileSystemNode
    {
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Required constructor for serializable
        /// <span class="code-SummaryComment"></summary>
</span>        public DirectoryTransport()
        {
            
        }

        /// <span class="code-SummaryComment"><summary>
</span>        ///     Constructor from interface
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="directory"></param>
</span>        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));
                }
            }
        }
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Name of the node
        /// <span class="code-SummaryComment"></summary>
</span>        public string Name { get; set; }
        
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Path to the node
        /// <span class="code-SummaryComment"></summary>
</span>        public string Path { get; set; }
                
        /// <span class="code-SummaryComment"><summary>
</span>        ///     Files 
        /// <span class="code-SummaryComment"></summary>
</span>        public List<FileTransport> FileContents { get; set; }

        /// <span class="code-SummaryComment"><summary>
</span>        ///     directories
        /// <span class="code-SummaryComment"></summary>
</span>        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Jeremy Likness
Architect Wintellect
United States United States
Jeremy Likness is a principal consultant at Wintellect. Jeremy, an experienced entrepreneur and technology executive, has successfully helped ship commercial enterprise software for 20 years. He specializes in catalyzing growth, developing ideas and creating value through delivering software in technical enterprises. His roles as business owner, technology executive and hands-on developer provided unique opportunities to directly impact the bottom line of multiple businesses by helping them grow and increase their organizational capacity while improving operational efficiency. He has worked with several initially small companies like Manhattan Associates and AirWatch before they grew large and experienced their transition from good to great while helping direct vision and strategy to embrace changing technology and markets. Jeremy is capable of quickly adapting to new paradigms and helps technology teams endure change by providing strong leadership, working with team members “in the trenches” and mentoring them in the soft skills that are key for engineers to bridge the gap between business and technology.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 19 Jun 2009
Article Copyright 2009 by Jeremy Likness
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid