最近做SonySource項目時實現了幾個很小的Silverlight程序,分別是Clock、HomePeoplePicker和ManageMentPeoplePicker。實際上這三個silverlight程序都非常簡單,主要特點有以下幾個方面:
1. Silverlight程序和頁面上的HTML元素混合在一起,且在特定事件觸發后要動態改變Silverlight程序在頁面中占的位置及大小,但給用戶的感覺是無縫連接;
2. Javascript和Silverlight相互調用;
3. 簡單的探照燈遮照效果;
下面就分別對我認為比較不好處理的地方或者一些我費了很多周折才實現的地方做一簡要說明:
一、使Silverlight浮動在Html元素中,并動態改變大小
或許我這個小標題描述得還不是很準確,不能直觀表達我的意思。舉個例子,假設我們要用Silverlight做個下拉菜單,并將他放在html頁面上使用。我們希望這個silverlight菜單所占的大小只是980px寬和30px高,因為在緊挨菜單的上面和下面的地方我們要放置一起其他的html元素。但當用戶點擊某個菜單項時,這個子菜單就展開,假設子菜單的大小是100px款和200px高,那就要求Siverlight所占的位置至少高為230px。由于Silverlight菜單只有30px高,所以下拉菜單就被截斷而不能完整顯示。我做的這個項目里三個Silverlight都遇到類似問題。例如PeoplePicker是在一個表格框里顯示很多人的圖像,當用戶點擊一個人的圖像的時候彈出一個窗口以顯示人的詳細信息,在某種情況下,這個彈出窗口會超出包含所有人物圖像的表格從而部分被截斷。在《Silverlight嵌入到HTML之windowless屬性及運用AjaxControlToolKit時出現虛線邊框的問題》所描述的問題就是基于這種需求。
上述問題是否可以簡單的描述為:Silverlight程序在頁面上只在指定的Silverlight plug in(<object/>元素)中顯示,當超過Silverlight Plug in時就會被截除;當Silverlight程序的寬和高在運行時不確定時,就要求Silverlight Plug in的大小和位置隨之改變以使所有silverlight內容都能完整正確的顯示出來。
我在這個項目里的解決辦法就是基于以上的描述,動態改變Silverlight plug in(object元素)的大小,并時silverlight plug in以絕對定位的方式浮動于其他元素之上,且讓silverlight plug in的背景色為透明以不至于讓他遮蓋所有的底層元素。
首先,我們在頁面上定義一個<div>元素,我們的silverlight程序就放在這個<div>里,并以它作為silverlight的定位基準。即正常情況下silverlight和包含它的<div>的位置和大小完全一致。當需要改變silverlight的大小和位置時,也以該<div>為參考。在頁面布局時,我們只用關注這個<div>應該放到哪就行了。HTML代碼大致如下:

Code
<div id="silverlightHomePeoplePickerHost" style="width:275px;height:324px;background-color:transparent;float:left">
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" style="width:100%;height:100%;position:absolute">
<param name="source" value="../ClientBin/SEL.SonySource.Silverlight.HomePeoplePicker.xap"/>
<param name="onerror" value="onSilverlightError" />
<param name="onload" value="onSilverlightHomePeoplePickerLoaded" />
<param name="background" value="transparent" />
<param name="windowless" value="true" />
<param name="minRuntimeVersion" value="2.0.31005.0" />
<param name="autoUpgrade" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none;">
<img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/>
a>
object>
<iframe style='visibility:hidden;height:0;width:0;border:0px'>iframe>
div>
那么怎么初始化silverlight的位置和大小呢?就在onload事件里處理:

Code
var silverlightHomePeoplePickerInstance = null;
// When loaded the silverlight HomePeoplePicke in html page
function onSilverlightHomePeoplePickerLoaded(sender, args) {
silverlightHomePeoplePickerInstance = new SilverlightHomePeoplePicker(sender);
}
在onSilverlightHomePeoplePickerLoaded事件中,首先通過參數sender創建一個SilverlightHomePeoplePicker的實例,SilverlightHomePeoplePicker對象的代碼大致如下:

Code
// the class of SilverlightHomePeoplePicker
function SilverlightHomePeoplePicker(sender) {
this.slApp = sender;
this.objElement = this.slApp.getHost();
this.divContainer = this.objElement.parentNode;
this.page = this.objElement.Content.Page;
// the left and top offset to the directly parent element of silverlight object tag
this.leftOffsetToContainer = 0;
this.topOffsetToContainer = 0;
this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDelegate(this, this.handleSilverlightScopeChangedEvent));
addEvent(window, "onresize", SilverlightHomePeoplePicker.createDelegate(this, this.setPosition));
with (this.objElement.style) {
position = "absolute";
zIndex = 999;
width = this.divContainer.offsetWidth + "px";
height = this.divContainer.offsetHeight + "px";
}
this.setPosition();
}
SilverlightHomePeoplePicker.prototype = {
// change scope of silverlight object tag
handleSilverlightScopeChangedEvent: function(s, e) {
this.leftOffsetToContainer = e.Left;
this.topOffsetToContainer = e.Top;
this.setPosition();
with (this.objElement.style) {
width = e.Width + "px";
height = e.Height + "px";
}
},
// set left and top positions
setPosition: function() {
setSilverlightObjectPosition(this.objElement, this.divContainer, this.leftOffsetToContainer, this.topOffsetToContainer);
}
}
SilverlightHomePeoplePicker.createDelegate = function(instance, method) {
return function() {
return method.apply(instance, arguments);
}
}
可以看到,在構造SilverlightHomePeoplePicker的實例時就設置了 <object/>元素的位置和大小。
leftOffsetToContainer和topOffsetToContainer是指silverlight plug in ( object 元素 )左上相對于包含它的<div>的左上角的偏移量,一般正常情況下,這個偏移量為0,即silverlight與包含它的<div>左上角位置重疊。初始化時同時設置了silveright插件的寬和高,即等于包含它的<div>的寬和高。
那么當Silverlight的位置和大小需要改變時怎么辦呢?誰來負責處理這個變化呢?首先,在Silverlight程序里應該最先知道這個Silverlight程序的范圍是否改變了,是否需要改變silverlight plug in的位置和大小來正確顯示整個Silverlight的內容。例如,當彈出或關閉詳細信息窗口時,Silverlight程序應該做這個檢查,如果需要改變silverlight plugin的位置和大小,就通知javascript程序。
Silverlight的啟動xmal文件Page.xaml大概是這個樣子:

Code
<UserControl x:Class="SEL.SonySource.Silverlight.HomePeoplePicker.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Width="275" Height="324">
<Grid x:Name="LayoutRoot" RenderTransformOrigin="0,0">
<Grid.RenderTransform>
<TranslateTransform x:Name="LayoutRootTranslate" X="0" Y="0" />
Grid.RenderTransform>
Grid>
UserControl>
然后定義一個SilverlightScopeChangeHandler:

Code
///
/// When the scope of the app changed, call the handler to invoke js to resize the position of the plug-in object
/// Most time, this will be triggered by popup or close the detail window
///
///
///
public delegate void SilverlightScopeChangedHandler(Object sender, ScopeEventArgs e);
///
/// Indicate the scope of the silverlight app
///
public class ScopeEventArgs : EventArgs
{
///
/// Relative to the orginal left position, it will always less than 0
///
[ScriptableMember]
public double Left
{
get;
internal set;
}
///
/// Relative to the orginal top position, it will always less than 0
///
[ScriptableMember]
public double Top
{
get;
internal set;
}
///
/// The actual width of the app
///
[ScriptableMember]
public double Width
{
get;
internal set;
}
///
/// The height width of the app
///
[ScriptableMember]
public double Height
{
get;
internal set;
}
public ScopeEventArgs(double left, double top, double width, double height) : base()
{
this.Left = left;
this.Top = top;
this.Width = width;
this.Height = height;
}
public static bool operator ==(ScopeEventArgs e1, ScopeEventArgs e2)
{
return Object.Equals(e1, e2);
}
public static bool operator !=(ScopeEventArgs e1, ScopeEventArgs e2)
{
return !Object.Equals(e1, e2);
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (GetType() != obj.GetType())
return false;
ScopeEventArgs e = (ScopeEventArgs)obj;
return (this.Left==e.Left && this.Top == e.Top && this.Width == e.Width && this.Height == e.Height );
}
}
在Page.cs頁面中,當捕獲到需要改變silverlight plugin(object元素)的位置和大小時,就觸發相應的SilverlightScopeChange事件:

Code
// scope changed event
[ScriptableMember]
public event SilverlightScopeChangedHandler SilverlightScopeChanged;
// save current silverlight scope
private ScopeEventArgs silverlightScope = null;
///
/// detect if the size of silverlight app is larged than orgianl size or if it's return to the origianl size
/// This will invoke the js to reposition the silverlight plug-in on the html page
///
private void DetectSilverlightScopeChange()
{
........
ScopeEventArgs se = new ScopeEventArgs(left, top, width - left, height - top );
if (silverlightScope != se)
{
silverlightScope = se;
LayoutRootTranslate.X = -silverlightScope.Left;
LayoutRootTranslate.Y = -silverlightScope.Top;
if (SilverlightScopeChanged != null)
SilverlightScopeChanged(this, silverlightScope);
}
}
當Silverlight應用程序檢查到需要改變 <object>元素的大小時,就觸發SilverlightScopeChanged事件,告知javascript來處理,同時SilverlightScopeChangeEventArgs參數還告訴了silverlight plug in需要的寬、高以及相對于上級<div>的偏移量。
到這里,大家就會看到上述SilverlightHomePeoplePicker代碼中
this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDelega
te(this, this.handleSilverlightScopeChangedEvent));
的意義所在了。
二、Silverlight程序和Javascript程序的相互調用
1. javascript注冊silverlight的事件
其實上面的代碼已經體現了這點,即通過javascript的代碼
this.page.addEventListener("SilverlightScopeChanged", SilverlightHomePeoplePicker.createDeleg
ate(this, this.handleSilverlightScopeChangedEvent));
來注冊silverlight端的事件。這里要注意幾點:
a. Silverlight事件必須是Scriptable的,即事件的聲明上加上[ScriptableMember];
b. 事件原型必須有兩個參數, sender和e。sender是object類型的,e需要時繼承自EventArgs類型的。我在這里走了些彎路。
c. 必須在Silverlight程序中注冊整個類以供javascript訪問,例如:HtmlPage.RegisterScriptableObject("Page", this);
d. silverlight程序在LayoutUpdated事件中是無法訪問或觸發任何javascript方法的。
2. silverlight注冊html element的事件
在silverlight中可以注冊html元素的事件。例如在clock程序中,要求當用戶點擊網頁上任何位置時都要關閉timezonelist下拉列表。這時可以在silverligh中注冊document.click事件:

Code
HtmlPage.Document.AttachEvent("onclick", Document_Click);
...
private void Document_Click(object sender, HtmlEventArgs e)
{
if (ListBoxMaskLayer.Visibility == Visibility.Visible)
HideTimezoneList();
}
但后來由于firefox下點擊silverlight時也觸發document.click事件,所以沒有采用這種方式。
3. Javascript調用Silverlight的方法9
由于沒有采用第2點所示的方法,所以document.click事件還是由javascript來處理: 但是,在handleClickDocumentEvent事件里,javascript調用了silverlight的方法:
this.page.HideTimezoneList();
與第1條所述類似,silverlight中HideTimezoneList這個方法必須標記為[ScriptableMember],且要注冊Page類到js:
HtmlPage.RegisterScriptableObject("Page", this);

Code
// the class of SilverlightClock
function SilverlightClock(slClock) {
...............
this.page = this.objElement.Content.Page
addEvent(document, "onclick", SilverlightClock.createDelegate(this, this.handleClickDocumentEvent));
.................
}
SilverlightClock.prototype = {
// when click on the document, shrink the timezone listbox
handleClickDocumentEvent: function(e) {
e = window.event || e;
var srcE = e.srcElement || e.target;
if (!srcE.source || srcE.source.indexOf("SEL.SonySource.Silverlight.Clock.xap") == -1) {
if (this.objElement.style.zIndex > 0) {
try{ //BUG: there will be an error in FF
this.page.HideTimezoneList();
}
catch(err){}
}
}
}
}
三、簡單探照燈效果的實現
這里的探照燈的實現,主要運用了RadialGradientBrush。在整個Silverlight的內容上面放一個Rectangle,且將其Fill為RadialGradientBrush:

Code
<Grid x:Name="LayoutRoot" RenderTransformOrigin="0,0" MouseMove="LayoutRoot_MouseMove" MouseEnter="LayoutRoot_MouseEnter">
<local:UniformGrid x:Name="PicturesContainerGrid" Width="275" Height="324" Columns="5" Rows="4" MinWidth="55" MinHeight="81" />
<Canvas x:Name="LamplightCanvas" Width="275" Height="324" MouseLeftButtonDown="LamplightCanvas_MouseLeftButtonDown" Cursor="Hand">
<Rectangle x:Name="LamplightRect" Width="700" Height="644" Stretch="Fill" RenderTransformOrigin="0.5,0.5" Canvas.Left="-208" Canvas.Top="-199" >
<Rectangle.Fill>
<RadialGradientBrush x:Name="LamplightGradient" RadiusX="0.5" RadiusY="0.666667" Center="0,0" GradientOrigin="0,0">
<GradientStop Color="#00000000" Offset="0.08"/>
<GradientStop Color="#7F000000" Offset="0.143"/>
<GradientStop Color="#7F000000" Offset="1"/>
<GradientStop Color="#00FFFFFF" Offset="0"/>
RadialGradientBrush>
Rectangle.Fill>
Rectangle>
<Canvas.Clip>
<GeometryGroup x:Name="LamplightClipGroup">
<RectangleGeometry Rect="0,0,275,324" />
<RectangleGeometry Rect="0,0,0,0" x:Name="MouseOverRectGemo" />
GeometryGroup>
Canvas.Clip>
Canvas>
Grid>
當鼠標移動時,就動態改變RadialGradientBrush的Center和GradientOrigin屬性。大致代碼如下:

Code
private void LayoutRoot_MouseMove(object sender, MouseEventArgs e)
{
HanleMouseEventOnLayoutRoot(e);
}
private void HanleMouseEventOnLayoutRoot(MouseEventArgs e)
{
// if the mouse move in the Main Grid
if (IsCaptureMouse(PicturesContainerGrid, e))
{
// if no people has been selected ,move the lamplight
if (dicPersonDetails.Count == 0)
{
// move lamplight
SetLamplight(e);
// open a small rect to enable triger the mouseover and click of HomePerson control
Point ptMouse = e.GetPosition(LamplightCanvas);
MouseOverRectGemo.Rect = new Rect(ptMouse.X - 2, ptMouse.Y - 2, 4, 4);
}
else
{
// Don't move lamplight because one HomePerson has alreay get the lamplight,
// but a big rect should open to trigger MouseOver of HomePerson Control
HomePerson homePerson = GetHomePerson(e);
if (homePerson != null)
{
// check if click on any detail window
foreach (KeyValuePair<HomePerson, HomeDetails> kv in dicPersonDetails)
{
if (kv.Value.IsOpened && IsCaptureMouse(kv.Value, e))
{
MouseOverRectGemo.Rect = new Rect(-1, -1, 0, 0);
return;
}
}
RemoveLamplightMask(homePerson);
}
}
}
}
private void SetLamplight(Point pointRelativeToLamplightRec)
{
Point ptCenter = new Point(pointRelativeToLamplightRec.X / LamplightRect.ActualWidth, pointRelativeToLamplightRec.Y / LamplightRect.ActualHeight);
LamplightGradient.Center = LamplightGradient.GradientOrigin = ptCenter;
}
///
/// set the focus lamplight to the mouse position
///
///
private void SetLamplight(MouseEventArgs e)
{
Point pointRelativeToLamplightRec = e.GetPosition(LamplightRect);
SetLamplight(pointRelativeToLamplightRec);
}
另外還有一個問題,由于在所有內容上遮照了一個Rectangle,那么當鼠標移到某個位置時,雖然燈亮了,那怎么觸發Rectangle下層元素的事件呢?我這里主要運用了包含Rectangle的Canvas的Clip屬性,在Clip里我定義了兩個Geometry,動態改變Geometry的位置,就相當于在Rectangle上動態打一些窟窿。
四、其他問題
當然還有一些需要改進的地方,例如Clock的點"edit"按鈕后彈出的下拉框的寬和高應該動態自適應,可以在TimezoneList的Curstom Control的MeasureOverride事件中獲取應該分配的寬和高,進而計算其他數據達到自適應的效果,簡單偽代碼如下:

Code
public event RequestMeasuredSizeHandler RequestMeasuredSize;
protected override Size MeasureOverride(Size availableSize)
{
Size desireSize = base.MeasureOverride(new Size(double.MaxValue,double.MaxValue));
if (RequestMeasuredSize != null)
RequestMeasuredSize(desireSize);
this.Width = desireSize.Width;
this.Height = desireSize.Height;
return desireSize;
}
還有,在探照燈已經固定在某一個人的頭像上后,當鼠標滑動時還可以使效果更加柔和。