代码浏览器第二版: Silverlight





4.00/5 (3投票s)
这篇文章将引导您完成我将一个完全没有 Silverlight 的项目转变为此处可用现有项目所采取的步骤……
上周发布了基于 AJAX 的源代码浏览器后,我决定迭代加入 Silverlight 支持。我们公司正在探索这项技术,所以这是一个完美的“概念验证”项目,可以在公司内部使用 Silverlight 之前熟悉它。
这篇文章将引导您完成我将一个完全没有 Silverlight 的项目转变为此处可用现有项目所采取的步骤。
我的第一步是对布局进行了一些基本的重构。如果您还记得,第一个版本使用了一个基础页面来连接 JavaScript 和样式表。因为我现在有两个页面,我决定将该功能提取到一个 MasterPage 中,以便我可以拥有一个通用的页眉/页脚等。这相当简单。
- 创建一个包含通用元素(包括脚本管理器和外部表单)的主页面
- 将逻辑从基础页面移动到主页面代码背后
- 将 _Default.aspx_ 重命名为 _Default_.aspx_,然后添加一个带有 MasterPage 引用的新 _Default.aspx_
- 移动相关项并进行测试
我的第一轮测试发现我的样式失效了,因为 CSS 正在寻找 `#tree` 来覆盖锚标签行为。因为这是一个在服务器上运行的 span,控制器可以渲染到其中,所以我将 `span` 元素上移并使用占位符来渲染控件。请注意 `span` 有一个“`local`”id,而占位符有一个服务器 id,它基本上采用了它前面的主页面的 id。它在 _.aspx_ 中看起来像这样
<span id="_tree">
<asp:PlaceHolder ID="_tree" runat="server"/>
</span>
但渲染出来是这样的
<span id="_tree">
<div id="ctl00__contentPlaceHolder1_ctl00__pnlTree">
... etc etc
下一步是准备模型以供服务使用。Silverlight 在浏览器中运行,因此无法直接访问任何现有程序集(它实际上有自己的精简版 CLR)。这变成了真正的“客户端-服务器”模式。虽然我们为 AJAX 版本使用了回调,但这将使用真正的服务。因为我的主要 Web 项目正在使用 `interface`,所以我需要一些具体的东西来进行序列化。我创建了一个名为 `Transport` 的文件夹,并开始构建我的对象。
我必须做的第一个更改是针对 _parent_ 目录。原始模型有一个 `IDirectory` 引用,这显然需要为服务具体化。因为我真的不需要 _parent_ 目录,所以我决定将其从原始 `interface` 中分离出来,并创建一个新的 `IParentDirectory` `interface`。然后 `FileModel` 和 `DirectoryModel` 分别简单地实现 `IFile, IParentDirectory` 和 `IDirectory, IParentDirectory`。
`FileTransport` 是第一个。我可以为此实现 `IFile` 并保持其“原样”。为了便于将我的 `interface` 类型转换为具体实例,我允许构造函数接收一个 `IFile` 并设置其属性。我还向构造函数添加了一个标志,以确定是否加载内容。在构建树时,我不会通过网络发送内容,只在请求实际源时发送。完成的类如下所示
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; }
}
}
`DirectoryTransport` 有点意思。我需要发送具体的目录和具体的文件,因此无法使用 `interface` 中定义的 `IList`。我决定实现 `IFileSystemNode`(仅包含名称和路径),然后创建了一个独立的 `List
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; }
}
}
传输对象使得连接服务变得非常容易。第一个提供完整树节点(不含代码)的服务就像这样简单
[WebMethod]
public DirectoryTransport GetMasterNode()
{
return new DirectoryTransport(TreeCache.GetMasterNode());
}
相同的缓存被使用,`IDirectory` 被送入 `DirectoryTransport`,然后通过网络序列化(您可以通过浏览服务然后调用它来在本地机器上查看它的样子)。
文件详细信息方法也很简单。给定一个路径,它会遍历树结构直到找到正确的文件(这还不智能 - 没有哈希或理解部分路径等 - 那将是重构),然后处理代码以 `string` 形式发送。
由于从字节转换为 `string`,我将 `Leaf` 中的转换部分提取出来,并将其放入 `Utility` 中,带有一个开关,以便用 `<pre>` 标签封装以进行语法高亮显示(可惜,Silverlight 版本尚不支持高亮显示或选择)。
你可以仔细查看代码来了解这一点。这些是对主代码库的修改。你可以看到拥有一个多层应用程序使得我们能够非常容易和直接地构建服务所需的各个部分!没有大的重构或对象重组,只需进行一些调整,我们就可以开始了。现在是好玩的部分……Silverlight 部分。
当然,所有 Silverlight 的先决条件都需要下载、安装并可用。我使用了最新的非 Beta 版本(截至本文撰写时为 2.0),并获得了工具包。我制作的页面非常简单直观。我创建 Silverlight 项目的步骤如下:
- 在我的解决方案中添加了一个新项目作为 Silverlight 应用程序。
- 当提示时,我选择将测试页面嵌入到现有的 Web 应用程序 (`CodeBrowser`) 中。
- 在我的 `CodeBrowser` 应用程序中,我创建了一个名为 _Silver.aspx_ 的新页面,并简单地添加了 Silverlight 控件和对应用程序的引用,然后删除了测试页面。我的 MasterPage 有一个指向 AJAX 和 Silverlight 版本的链接。
- 最后,我添加了对 _DataAccess.asmx_ 服务的引用,以获取树和文件内容
Xaml 很简单——一个堆叠面板,带有一个树视图和一个代码滚动视图器,并用 Expression Blend 注入了一些样式、颜色和字体。它看起来像这样:
<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>
代码背后比我预期的要容易。首先,我实例化了我的 Web 服务代理。
...
readonly DataAccessSoapClient _service = new DataAccessSoapClient();
...
接下来,连接一些事件来监听服务,并启动服务来加载主节点(在构造函数中完成)
public Page()
{
InitializeComponent();
_service.GetMasterNodeCompleted += _ServiceGetMasterNodeCompleted;
_service.GetFileDetailsCompleted += _ServiceGetFileDetailsCompleted;
_service.GetMasterNodeAsync();
}
最后,在事件中,我连接了两个活动。获取节点开始构建嵌套 `TreeViewItem` 的过程,我将其添加到控件中。我在目录上连接了一个点击事件,以便它们可以切换。
static void _DirectorySelected(object sender, System.Windows.RoutedEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
if (item != null)
{
item.IsExpanded = !item.IsExpanded;
}
}
我递归遍历节点,根据需要创建更多项。请注意,对于文件,我绑定了一个单独的点击事件,并将 `FileTransport` 作为该节点的 `DataContext`
foreach (FileTransport file in node.FileContents)
{
TreeViewItem fileItem = new TreeViewItem {DataContext = file, Header = file.Name};
fileItem.Selected += _FileItemSelected;
root.Items.Add(fileItem);
}
以后我可能会重构使用模板,但目前这样效果很好。当文件被选中时,我们获取数据上下文并将其转换回文件,然后调用服务以获取实际代码。
FileTransport file = item.DataContext as FileTransport;
if (file != null)
{
svCode.Content = new TextBlock {Text = "Loading..."};
_service.GetFileDetailsAsync(file.Path);
}
最简单的是,当调用返回时,我们只需将代码注入到 `ScrollView` 控件中。
void _ServiceGetFileDetailsCompleted
(object sender, GetFileDetailsCompletedEventArgs e)
{
svCode.Content = e.Result;
}
就这样!我发布了它,并且能够运行它。显然,还有很多工作要做。AJAX 方面需要停止加载整个树,而是根据节点展开进行延迟加载。Silverlight 方面需要一些高亮显示和 UI 清理。然而,希望如果您是 Silverlight 的新手,这个逐步指南对于采用现有代码并进行连接,以及使用 Silverlight 中的一些新控件是有用的。
下次再见……