從網頁應用程式時代開始,程式設計師與版面設計師間如何分工,便是一個難解的習題,程式設計師的專長是寫程式,而版面設計師的專長是做出美美的網頁,
Silverlight 2 - 程式設計師與版面設計師的分工
文/黃忠成
談真實Silverlight 2的應用程式開發
從網頁應用程式時代開始,程式設計師與版面設計師間如何分工,便是一個難解的習題,程式設計師的專長是寫程式,而版面設計師的專長是做出美美的網頁,
程式設計師多半缺乏美感,而版面設計師多半不會寫程式,專案主導者總是嘗試在兩者間找出一個合作流程,但結局多半不如預期。
我們都知道,最好的情況應該是,版面設計師先將版面設計好,再交給程式設計師補上程式流程,但問題是,版面設計師由於對程式不熟悉,因此對於頁面的切換,
按下按紐後的行為等動作難以著墨,最後這些事依然回到程式設計師身上,專案在兩者間流動是常見的事,既然流動,必然也會發生程式流程影響原來版面,或是版面
無法在程式流程中維持不變。結局多半是,版面設計師被迫學會JavaScript+部份的ASP.NET程式,而程式設計師被迫學會小幅的版面配置美學。
同樣的情況在Silverlight 2應用程式開發時也發生,先天上的架構,讓Silverlight 2應用程式開發可以分成兩個部份,一是單純XAML的畫面設計,二是事件觸發、
資料繫結的程式撰寫,就分工上而言,單純XAML的畫面設計工作應該交給版面設計師來處理,而事件觸發、資料繫結部份則交給程式設計師。
但事情並非如此順利,以一個簡單的ListBox+Item動畫來說,這原本應該是版面設計師的工作,因為Item被選取時的動畫,應該是由版面設計師所設計的,
但ListBox的Item來自資料繫結,如何讓版面設計師於Blend 2上就可以繫結資料,成了一大難題。
另一方面,有時版面設計師會希望,當使用者將滑鼠移往某控件上時撥放一段動畫,而這段動畫自然是由版面設計師來做,但滑鼠移往某控件上然後撥放動畫,
這卻會落到程式設計師頭上,至少!在Silverlight 2及Blend 2上是如此。
關於設計時期資料繫結
Blend 2在設計Silverlight 2專案時,並不如WPF專案般允許XML資料繫結,因此要達到設計時期資料繫結動作的唯一途徑是使用CLR物件,如圖1、2、3所示。
圖1
圖2
圖3
當擁有資料繫結能力後,版面設計師就能在不寫半行程式碼的情況下,做出類似當滑鼠移往某個Item項目後的動畫展現,且可以立即以F5來執行看成果。
如果搭配上程式設計師開發由ListBox繼承而來的自訂控件,那麼版面設計師能做的事就更多了。
另外,值得一提的是,CLR物件取得的資料可以來自很多來源,XML或是Web Services皆可,這可讓版面設計師能於設計時期得到真實的資料,以做出趨近需求的版面。
(PS: Blend 3 Preview提供了XML Binding機制,可讓設計師於設計時期由XML進行Binding)
下面是SLDataProvider的原始碼,這是一個Silverlight Class Library專案,裡面只有一個.cs檔案(程式1),當使用Blend 2時,只須將此專案加入參考即可透過CLR物件來使用Data Binding機制。
程式1
using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; using System.Collections.Generic; namespace SLDataProvider { public class Class1 { private static List<string> _data = new List<string>(); public List<string> Data { get { if (_data.Count == 0) { try { System.Windows.Application.Current.Host.Source.ToString(); ReadServer(); } catch (Exception) { ReadLocal(); } } return _data; } } private static void ReadLocal() { using (FileStream fs = new FileStream(@"C:\ttt2.txt", FileMode.Open, FileAccess.Read)) { using (StreamReader sr = new StreamReader(fs)) { while (!sr.EndOfStream) { _data.Add(sr.ReadLine()); } } } } public static void ReadServer() { WebClient wc = new WebClient(); wc.OpenReadCompleted += new OpenReadCompletedEventHandler(wc_OpenReadCompleted); wc.OpenReadAsync(new Uri("http://localhost:82/ttt2.txt", UriKind.Absolute)); } static void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { _data.Clear(); using (StreamReader sr = new StreamReader(e.Result)) { while (!sr.EndOfStream) { _data.Add(sr.ReadLine()); } } } } } |
特別注意一點,在Data的屬性存取子上,我們利用了Host.Source.ToString();來判斷目前是處於設計時期或執行時期,當這段程式碼可運行,沒有產生例外時,就是處於執行時期,
反之則是處於設計時期,在設計時期我們直接讀取本地端的XML,反之則讀取Server端的XML。
另外,由於Silverlight的網路動作皆為非同步,所以在.cs中,必須意識到此點,運用DispatcherTimer來重新設置ItemsSource屬性,如下所示:
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; namespace SilverlightApplication6 { public partial class Page : UserControl { public Page() { InitializeComponent(); } private void Storyboard_Completed(object sender, EventArgs e) { } private void UserControl_Loaded(object sender, RoutedEventArgs e) { System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(1200); timer.Tick += new EventHandler(timer_Tick); timer.Start(); } void timer_Tick(object sender, EventArgs e) { object o = lst2.ItemsSource; lst2.ItemsSource = null; lst2.ItemsSource = o as IEnumerable; timer.Stop(); } } } |
PS: OK,我知道用DispatcherTimer來做很偷懶,不過我相信你會找到更好的解法,例如透過Tag或是下面的Custom Attached Properties協助,
讓重置ItemsSource的動作變的更優雅。
事件觸發
相對於資料繫結問題,事件觸發後的動畫撥放問題就較為麻煩,就實務情況而言,能不能讓版面設計師在不寫半行程式情況下,做出當滑鼠移往控件上時撥放一段動畫,
是能否將版面設計與程式設計完全切離的關鍵。
不幸的是,Blend 2對於Silverlight 2專案的設計,並不支援Loaded外的Event Trigger,這使得版面設計師無法在不寫程式的情況下,做出前面所提到的效果,
結局多半是版面設計師得寫上一小段程式才能完成,要達到不寫程式而完成需求,直到Silverlight 3及Blend 3推出之前,都很難做到。
在Blend 2上,對此我的回答是Custom Attached Properties,也就是附加屬性機制,透過這個機制,我們能撰寫一個Silverlight Class Library專案,於內加入一個類別,
然後將屬性附加到所有控件上,在使用者設定屬性值後自動掛載事件來撥放指定動畫,搭配上網路上可得到的自動XAML完成機制,可以做到未來Blend 3會提供的
MouseOver、Click等Event Trigger功能,下面是此類別的原始碼。
程式3
using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Windows.Controls.Primitives; using System.Collections.Generic; namespace SLTrigger { public class Trigger { public static readonly DependencyProperty Trigger_ClickProperty = DependencyProperty.RegisterAttached( "Click", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_MouseOverProperty = DependencyProperty.RegisterAttached( "MouseOver", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_MouseLeaveProperty = DependencyProperty.RegisterAttached( "MouseLeave", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_LostFocusProperty = DependencyProperty.RegisterAttached( "LostFocus", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_GotFocusProperty = DependencyProperty.RegisterAttached( "GotFocus", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_SelectionChangedProperty = DependencyProperty.RegisterAttached( "SelectionChanged", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_TargetProperty = DependencyProperty.RegisterAttached( "Target", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_ZIndexProperty = DependencyProperty.RegisterAttached( "ZIndex", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); public static readonly DependencyProperty Trigger_SelectedProperty = DependencyProperty.RegisterAttached( "Selected", typeof(string), typeof(Trigger), new PropertyMetadata( new PropertyChangedCallback(OnClickPropertyChanged))); private static Dictionary<object, List<object>> tempZIndex = new Dictionary<object, List<object>>(); private static void OnClickPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { } private static UserControl GetUserControl(FrameworkElement start) { if (start.Parent is UserControl) return start.Parent as UserControl; return GetUserControl((FrameworkElement)start.Parent); } public static void SetClick(UIElement element, string value) { if (element is ButtonBase) { ButtonBase bb = (ButtonBase)element; bb.Click -= new RoutedEventHandler(ButtonClickEvent); bb.SetValue(Trigger_ClickProperty, value); if (value != string.Empty && value != null) bb.Click += new RoutedEventHandler(ButtonClickEvent); return; } element.MouseLeftButtonUp -= new MouseButtonEventHandler(ClickEvent); element.SetValue(Trigger_ClickProperty, value); if (value != string.Empty && value != null) element.MouseLeftButtonUp += new MouseButtonEventHandler(ClickEvent); } public static string GetClick(UIElement element) { object value = element.GetValue(Trigger_ClickProperty); if (value == null) return string.Empty; return (string)value; } public static void SetMouseOver(UIElement element, string value) { element.MouseEnter -= new MouseEventHandler(MouseOverEvent); element.SetValue(Trigger_MouseOverProperty, value); if (value != string.Empty && value != null) element.MouseEnter += new MouseEventHandler(MouseOverEvent); } public static string GetMouseOver(UIElement element) { object value = element.GetValue(Trigger_MouseOverProperty); if (value == null) return string.Empty; return (string)value; } public static void SetMouseLeave(UIElement element, string value) { element.MouseEnter -= new MouseEventHandler(MouseLeaveEvent); element.SetValue(Trigger_MouseLeaveProperty, value); if (value != string.Empty && value != null) element.MouseEnter += new MouseEventHandler(MouseLeaveEvent); } public static string GetMouseLeave(UIElement element) { object value = element.GetValue(Trigger_MouseLeaveProperty); if (value == null) return string.Empty; return (string)value; } public static void SetGotFocus(UIElement element, string value) { element.GotFocus -= new RoutedEventHandler(GotFocusEvent); element.SetValue(Trigger_GotFocusProperty, value); if (value != string.Empty && value != null) element.GotFocus += new RoutedEventHandler(GotFocusEvent); } public static string GetGotFocus(UIElement element) { object value = element.GetValue(Trigger_GotFocusProperty); if (value == null) return string.Empty; return (string)value; } public static void SetLostFocus(UIElement element, string value) { element.LostFocus -= new RoutedEventHandler(LostFocusEvent); element.SetValue(Trigger_LostFocusProperty, value); if (value != string.Empty && value != null) element.LostFocus += new RoutedEventHandler(LostFocusEvent); } public static string GetLostFocus(UIElement element) { object value = element.GetValue(Trigger_LostFocusProperty); if (value == null) return string.Empty; return (string)value; } public static void SetZIndex(UIElement element, string value) { element.SetValue(Trigger_ZIndexProperty, value); } public static string GetZIndex(UIElement element) { object value = element.GetValue(Trigger_ZIndexProperty); if (value == null) return string.Empty; return (string)value; } public static void SetSelected(UIElement element, string value) { element.SetValue(Trigger_SelectedProperty, value); } public static string GetSelected(UIElement element) { object value = element.GetValue(Trigger_SelectedProperty); if (value == null) return string.Empty; return (string)value; } public static void SetTarget(UIElement element, string value) { element.SetValue(Trigger_TargetProperty, value); } public static string GetTarget(UIElement element) { object value = element.GetValue(Trigger_TargetProperty); if (value == null) return string.Empty; return (string)value; } public static void SetSelectionChanged(UIElement element, string value) { if(element is Selector) { Selector sel = (Selector)element; sel.SelectionChanged -= new SelectionChangedEventHandler(SelectionChangedEvent); element.SetValue(Trigger_SelectionChangedProperty, value); if (value != string.Empty && value != null) sel.SelectionChanged += new SelectionChangedEventHandler(SelectionChangedEvent); } else if (element is TabControl) { TabControl sel = (TabControl)element; sel.SelectionChanged -= new SelectionChangedEventHandler(SelectionChangedEvent); element.SetValue(Trigger_SelectionChangedProperty, value); if (value != string.Empty && value != null) sel.SelectionChanged += new SelectionChangedEventHandler(SelectionChangedEvent); } } public static string GetSelectionChanged(UIElement element) { object value = element.GetValue(Trigger_SelectionChangedProperty); if (value == null) return string.Empty; return (string)value; } public static void ButtonClickEvent(object sender, EventArgs e) { ClickEvent(sender, null); } public static void ClickEvent(object sender, MouseButtonEventArgs e) { string ani = GetClick(sender as UIElement); if (ani != string.Empty) { UserControl uc = GetUserControl(sender as FrameworkElement); Storyboard sb = uc.FindName(ani) as Storyboard; Begin_Storyboard((UIElement)sender, sb); } } public static void MouseOverEvent(Object sender, MouseEventArgs e) { string ani = GetMouseOver(sender as UIElement); if (ani != string.Empty) { UserControl uc = GetUserControl(sender as FrameworkElement); Storyboard sb = uc.FindName(ani) as Storyboard; Begin_Storyboard((UIElement)sender, sb); } } public static void MouseLeaveEvent(Object sender, MouseEventArgs e) { string ani = GetMouseLeave(sender as UIElement); if (ani != string.Empty) { UserControl uc = GetUserControl(sender as FrameworkElement); Storyboard sb = uc.FindName(ani) as Storyboard; Begin_Storyboard((UIElement)sender, sb); } } public static void GotFocusEvent(Object sender, RoutedEventArgs e) { string ani = GetGotFocus(sender as UIElement); if (ani != string.Empty) { UserControl uc = GetUserControl(sender as FrameworkElement); Storyboard sb = uc.FindName(ani) as Storyboard; Begin_Storyboard((UIElement)sender, sb); } } public static void LostFocusEvent(Object sender, RoutedEventArgs e) { string ani = GetLostFocus(sender as UIElement); if (ani != string.Empty) { UserControl uc = GetUserControl(sender as FrameworkElement); Storyboard sb = uc.FindName(ani) as Storyboard; Begin_Storyboard((UIElement)sender, sb); } } private static void Begin_Storyboard(UIElement elem, Storyboard sb) { UserControl uc = GetUserControl(elem as FrameworkElement); if (sb != null) { sb.Stop(); string aniTarget = GetTarget((UIElement)elem); if (aniTarget != string.Empty) { if (aniTarget == "page") { UIElement aniTargetObj = uc; Storyboard.SetTarget(sb, aniTargetObj); string zIndex = GetZIndex(elem); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(aniTargetObj); if (!tempZIndex.ContainsKey(elem)) tempZIndex.Add(sb, new List<object>() { aniTargetObj, oldZIndex }); Canvas.SetZIndex(aniTargetObj, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } else { UIElement aniTargetObj = uc.FindName(aniTarget) as UIElement; Storyboard.SetTarget(sb, aniTargetObj); string zIndex = GetZIndex(elem); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(aniTargetObj); if (!tempZIndex.ContainsKey(elem)) tempZIndex.Add(sb, new List<object>() { aniTargetObj, oldZIndex }); Canvas.SetZIndex(aniTargetObj, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } } sb.Begin(); } } static void sb_Completed(object sender, EventArgs e) { if(tempZIndex.ContainsKey(sender)) { List<object> state = tempZIndex[sender]; Canvas.SetZIndex((UIElement)state[0], (int)state[1]); tempZIndex.Remove(sender); } } public static void SelectionChangedEvent(Object sender, SelectionChangedEventArgs e) { string ani = GetSelectionChanged(sender as UIElement); UserControl uc = GetUserControl(sender as FrameworkElement); if (ani != string.Empty) { if (ani.ToLower() == "item") { object currentItem = null; if (sender is Selector && ((Selector)sender).SelectedItem != null) currentItem = ((Selector)sender).SelectedItem; else if (sender is TabControl && ((TabControl)sender).SelectedItem != null) currentItem = ((TabControl)sender).SelectedItem; string selAni = GetSelected(currentItem as UIElement); if (selAni != string.Empty) { Storyboard sb = uc.FindName(selAni) as Storyboard; string selTarget = GetTarget(currentItem as UIElement); sb.Stop(); if (selTarget != string.Empty) { UIElement aniTargetObj = uc.FindName(selTarget) as UIElement; Storyboard.SetTarget(sb, aniTargetObj); string zIndex = GetZIndex(aniTargetObj); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(aniTargetObj); if (!tempZIndex.ContainsKey(aniTargetObj)) tempZIndex.Add(sb, new List<object>() { aniTargetObj, oldZIndex }); Canvas.SetZIndex(aniTargetObj, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } else { Storyboard.SetTarget(sb, currentItem as UIElement); string zIndex = GetZIndex(currentItem as UIElement); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(currentItem as UIElement); if (!tempZIndex.ContainsKey(currentItem as UIElement)) tempZIndex.Add(sb, new List<object>() { currentItem as UIElement, oldZIndex }); Canvas.SetZIndex(currentItem as UIElement, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } sb.Begin(); } } else { Storyboard sb = uc.FindName(ani) as Storyboard; if (sb != null) { sb.Stop(); string aniTarget = GetTarget((UIElement)sender); if (aniTarget != string.Empty) { UIElement aniTargetObj = uc.FindName(aniTarget) as UIElement; Storyboard.SetTarget(sb, aniTargetObj); string zIndex = GetZIndex(sender as UIElement); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(sender as UIElement); if (!tempZIndex.ContainsKey(sender)) tempZIndex.Add(sb, new List<object>() { aniTargetObj, oldZIndex }); Canvas.SetZIndex(aniTargetObj, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } else { if (sender is Selector && ((Selector)sender).SelectedItem != null) { Storyboard.SetTarget(sb, ((Selector)sender).SelectedItem as DependencyObject); string zIndex = GetZIndex(sender as UIElement); if (zIndex != string.Empty) { int oldZIndex = Canvas.GetZIndex(((Selector)sender).SelectedItem as UIElement); if (!tempZIndex.ContainsKey(sender)) tempZIndex.Add(sb, new List<object>() { ((Selector)sender).SelectedItem, oldZIndex }); Canvas.SetZIndex( ((Selector)sender).SelectedItem as UIElement, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } else if (sender is TabControl && ((TabControl)sender).SelectedItem != null) { Storyboard.SetTarget(sb, ((TabControl)sender).SelectedItem as DependencyObject); string zIndex = GetZIndex(sender as UIElement); int oldZIndex = Canvas.GetZIndex(((Selector)sender).SelectedItem as UIElement); if (!tempZIndex.ContainsKey(sender)) tempZIndex.Add(sb, new List<object>() { ((TabControl)sender).SelectedItem, oldZIndex }); Canvas.SetZIndex( ((Selector)sender).SelectedItem as UIElement, int.Parse(zIndex)); sb.Completed -= new EventHandler(sb_Completed); sb.Completed += new EventHandler(sb_Completed); } } sb.Begin(); } } } } } } |
藉助此Class Library的協助,下面的XAML碼得以順利執行,不需要寫任何程式碼。
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SilverlightApplication19.Page" xmlns:trigger="clr-namespace:SLTrigger;assembly=SLTrigger" Width="640" Height="480"> <UserControl.Resources> <Storyboard x:Name="Storyboard1"> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"> <SplineDoubleKeyFrame KeyTime="00:00:00.9000000" Value="264"/> <SplineDoubleKeyFrame KeyTime="00:00:01.2000000" Value="195"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)"> <SplineDoubleKeyFrame KeyTime="00:00:00.9000000" Value="12"/> <SplineDoubleKeyFrame KeyTime="00:00:01.2000000" Value="218"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White"> <Button Height="36" trigger:Trigger.Click="Storyboard1" HorizontalAlignment="Left" Margin="180,58,0,0" VerticalAlignment="Top" Width="87" Content="Button"/> <Ellipse Height="88" Margin="267,151,269,0" VerticalAlignment="Top" Stroke="#FF000000" RenderTransformOrigin="0.5,0.5" x:Name="ellipse"> <Ellipse.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </Ellipse.RenderTransform> <Ellipse.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF000000"/> <GradientStop Color="#FFE2C6C6" Offset="1"/> </LinearGradientBrush> </Ellipse.Fill> </Ellipse> </Grid> </UserControl> |
範例下載:
SLTrigger(設計時期Event Trigger支援)