逐步解說:建立可攜式 F# 程式庫
您可以遵循本逐步解說,以 F# 建立可搭配 Silverlight 應用程式、傳統桌面應用程式或 Windows 市集 應用程式 (使用 .NET 應用程式開發介面所建立) 使用的組件。如此一來,您就可以用其他 .NET 語言撰寫應用程式的 UI 部分 (例如 C# 或 Visual Basic),而用 F# 撰寫演算法部分。您也可以支援以不同平台為目標的不同使用者介面。
您無法直接從 F# 使用 Windows 市集 UI,因此建議您以其他 .NET 語言撰寫 Windows 市集 應用程式的 UI,並在可攜式程式庫中撰寫 F# 程式碼。您可以直接以 F# 撰寫 Silverlight 和 Windows Presentation Foundation (WPF) UI,但是您可能會想要利用當您在 Visual Studio 中撰寫 C# 或 Visual Basic 程式碼時可使用的其他設計工具。
必要條件
若要建立 Windows 市集 應用程式,您的開發電腦上必須具有 Windows 8。
若要建立 Silverlight 專案,您的開發電腦上必須具有 Silverlight 5。
試算表應用程式
在這個逐步解說中,您將開發向使用者呈現方格並在其儲存格中接受數值輸入與公式的簡單試算表。F# 層會處理和驗證所有輸入,特別是剖析公式文字並計算公式的結果。首先,您將建立 F# 演算法程式碼,包括剖析包含儲存格參考、數字和算術運算子之運算式的程式碼。這個應用程式也包含程式碼,以追蹤當使用者更新另一個儲存格內容時必須更新哪些儲存格。接下來,您會建立使用者介面。
下圖顯示您將會在此逐步解說中建立的應用程式。
試算表應用程式使用者介面
此逐步解說含有下列章節。
How To: Create an F# Portable Library
How To: Create a Silverlight App that Uses an F# Portable Library
How To: Create a ... Style App That Uses an F# Portable Library
How to: Create a Desktop App That References a Portable Library That Uses F#
HOW TO:建立 F# 可攜式程式庫
在功能表列上選擇 [檔案]、[新增專案]。在 [新增專案] 對話方塊中,展開 [Visual F#],選擇 [F# 可攜式程式庫] 專案類型,然後將程式庫命名為 Spreadsheet。請注意,專案會參考 FSharp.Core 的特別版本。
展開 [方案總管] 中的 [References] 節點,然後選取 [FSharp.Core] 節點。在 [屬性] 視窗中,[FullPath] 屬性的值必須包含 .NETPortable,表示您使用核心 F# 程式庫的可攜式版本。您也可以檢閱預設您可存取的各種 .NET 程式庫。這些程式庫都會使用已定義為 .NET 可攜式的 .NET Framework 通用子集。您可以移除不需要的參考,不過,如果您加入參考時,您的參考組件必須可在您以之為目標的平台上使用。組件的文件通常會表示其可用的平台。
開啟專案的捷徑功能表,然後選擇 [屬性]。在 [應用程式] 索引標籤上,目標 Framework 已設定為 [.NET 可攜式子集]。對於 Visual Studio 2012,這個子集適用的 .NET 目標是 Windows 市集 應用程式、.NET Framework 4.5 和 Silverlight 5。做為可攜式程式庫,您的應用程式必須憑藉各種平台提供的執行階段執行,因此這些設定很重要。Windows 市集 應用程式和 Silverlight 5 的執行階段包含完整 .NET Framework 的子集。
將主要程式碼檔重新命名為 Spreadsheet.fs,然後將下列程式碼貼入編輯器視窗。這個程式碼會定義基本的試算表功能。
namespace Portable.Samples.Spreadsheet open System open System.Collections.Generic [<AutoOpen>] module Extensions = type HashSet<'T> with member this.AddUnit(v) = ignore( this.Add(v) ) type internal Reference = string /// Result of formula evaluation [<RequireQualifiedAccess>] type internal EvalResult = | Success of obj | Error of string /// Function that resolves reference to value. /// If formula that computes value fails, this function should also return failure. type internal ResolutionContext = Reference -> EvalResult /// Parsed expression [<RequireQualifiedAccess>] type internal Expression = | Val of obj | Ref of Reference | Op of (ResolutionContext -> list<Expression> -> EvalResult) * list<Expression> with member this.GetReferences() = match this with | Expression.Ref r -> Set.singleton r | Expression.Val _ -> Set.empty | Expression.Op (_, args) -> (Set.empty, args) ||> List.fold (fun acc arg -> acc + arg.GetReferences()) [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] module internal Operations = let eval (ctx : ResolutionContext) = function | Expression.Val v -> EvalResult.Success v | Expression.Ref r -> ctx r | Expression.Op (f, args) -> try f ctx args with e -> EvalResult.Error e.Message type private Eval = Do with member this.Return(v) = EvalResult.Success v member this.ReturnFrom(v) = v member this.Bind(r, f) = match r with | EvalResult.Success v -> f v | EvalResult.Error _-> r let private mkBinaryOperation<'A, 'R> (op : 'A -> 'A -> 'R) ctx = function | [a; b] -> Eval.Do { let! ra = eval ctx a let! rb = eval ctx b match ra, rb with | (:? 'A as ra), (:? 'A as rb) -> return op ra rb | _ -> return! EvalResult.Error "Unexpected type of argument" } | _ -> EvalResult.Error "invalid number of arguments" let add = mkBinaryOperation<float, float> (+) let sub = mkBinaryOperation<float, float> (-) let mul = mkBinaryOperation<float, float> (*) let div = mkBinaryOperation<float, float> (/) let ge = mkBinaryOperation<float, bool> (>=) let gt = mkBinaryOperation<float, bool> (>) let le = mkBinaryOperation<float, bool> (<=) let lt = mkBinaryOperation<float, bool> (<) let eq = mkBinaryOperation<IComparable, bool> (=) let neq = mkBinaryOperation<IComparable, bool> (<>) let mmax = mkBinaryOperation<float, float> max let mmin = mkBinaryOperation<float, float> min let iif ctx = function | [cond; ifTrue; ifFalse] -> Eval.Do { let! condValue = eval ctx cond match condValue with | :? bool as condValue-> let e = if condValue then ifTrue else ifFalse return! eval ctx e | _ -> return! EvalResult.Error "Condition should be evaluated to bool" } | _ -> EvalResult.Error "invalid number of arguments" let get (name : string) = match name.ToUpper() with | "MAX" -> mmax | "MIN" -> mmin | "IF" -> iif | x -> failwithf "unknown operation %s" x module internal Parser = let private some v (rest : string) = Some(v, rest) let private capture pattern text = let m = System.Text.RegularExpressions.Regex.Match(text, "^(" + pattern + ")(.*)") if m.Success then some m.Groups.[1].Value m.Groups.[2].Value else None let private matchValue pattern = (capture @"\s*") >> (Option.bind (snd >> capture pattern)) let private matchSymbol pattern = (matchValue pattern) >> (Option.bind (snd >> Some)) let private (|NUMBER|_|) = matchValue @"-?\d+\.?\d*" let private (|IDENTIFIER|_|) = matchValue @"[A-Za-z]\w*" let private (|LPAREN|_|) = matchSymbol @"\(" let private (|RPAREN|_|) = matchSymbol @"\)" let private (|PLUS|_|) = matchSymbol @"\+" let private (|MINUS|_|) = matchSymbol @"-" let private (|GT|_|) = matchSymbol @">" let private (|GE|_|) = matchSymbol @">=" let private (|LT|_|) = matchSymbol @"<" let private (|LE|_|) = matchSymbol @"<=" let private (|EQ|_|) = matchSymbol @"=" let private (|NEQ|_|) = matchSymbol @"<>" let private (|MUL|_|) = matchSymbol @"\*" let private (|DIV|_|) = matchSymbol @"/" let private (|COMMA|_|) = matchSymbol @"," let private operation op args rest = some (Expression.Op(op, args)) rest let rec private (|Factor|_|) = function | IDENTIFIER(id, r) -> match r with | LPAREN (ArgList (args, RPAREN r)) -> operation (Operations.get id) args r | _ -> some(Expression.Ref id) r | NUMBER (v, r) -> some (Expression.Val (float v)) r | LPAREN(Logical (e, RPAREN r)) -> some e r | _ -> None and private (|ArgList|_|) = function | Logical(e, r) -> match r with | COMMA (ArgList(t, r1)) -> some (e::t) r1 | _ -> some [e] r | rest -> some [] rest and private (|Term|_|) = function | Factor(e, r) -> match r with | MUL (Term(r, rest)) -> operation Operations.mul [e; r] rest | DIV (Term(r, rest)) -> operation Operations.div [e; r] rest | _ -> some e r | _ -> None and private (|Expr|_|) = function | Term(e, r) -> match r with | PLUS (Expr(r, rest)) -> operation Operations.add [e; r] rest | MINUS (Expr(r, rest)) -> operation Operations.sub [e; r] rest | _ -> some e r | _ -> None and private (|Logical|_|) = function | Expr(l, r) -> match r with | GE (Logical(r, rest)) -> operation Operations.ge [l; r] rest | GT (Logical(r, rest)) -> operation Operations.gt [l; r] rest | LE (Logical(r, rest)) -> operation Operations.le [l; r] rest | LT (Logical(r, rest)) -> operation Operations.lt [l; r] rest | EQ (Logical(r, rest)) -> operation Operations.eq [l; r] rest | NEQ (Logical(r, rest)) -> operation Operations.neq [l; r] rest | _ -> some l r | _ -> None and private (|Formula|_|) (s : string) = if s.StartsWith("=") then match s.Substring(1) with | Logical(l, t) when System.String.IsNullOrEmpty(t) -> Some l | _ -> None else None let parse text = match text with | Formula f -> Some f | _ -> None type internal CellReference = string module internal Dependencies = type Graph() = let map = new Dictionary<CellReference, HashSet<CellReference>>() let ensureGraphHasNoCycles(cellRef) = let visited = HashSet() let rec go cycles s = if Set.contains s cycles then failwith ("Cycle detected:" + (String.concat "," cycles)) if visited.Contains s then cycles else visited.AddUnit s if map.ContainsKey s then let children = map.[s] ((Set.add s cycles), children) ||> Seq.fold go |> (fun cycle -> Set.remove s cycles) else cycles ignore (go Set.empty cellRef) member this.Insert(cell, parentCells) = for p in parentCells do let parentSet = match map.TryGetValue p with | true, set -> set | false, _ -> let set = HashSet() map.Add(p, set) set parentSet.AddUnit cell try ensureGraphHasNoCycles cell with _ -> this.Delete(cell, parentCells) reraise() member this.GetDependents(cell) = let visited = HashSet() let order = Queue() let rec visit curr = if not (visited.Contains curr) then visited.AddUnit curr order.Enqueue(curr) match map.TryGetValue curr with | true, children -> for ch in children do visit ch | _ -> () visit cell order :> seq<_> member this.Delete(cell, parentCells) = for p in parentCells do map.[p].Remove(cell) |> ignore type Cell = { Reference : CellReference Value : string RawValue : string HasError : bool } type RowReferences = { Name : string Cells : string[] } type Spreadsheet(height : int, width : int) = do if height <=0 then failwith "Height should be greater than zero" if width <=0 || width > 26 then failwith "Width should be greater than zero and lesser than 26" let rowNames = [| for i = 0 to height - 1 do yield string (i + 1)|] let colNames = [| for i = 0 to (width - 1) do yield string (char (int 'A' + i)) |] let isValidReference (s : string) = if s.Length < 2 then false else let c = s.[0..0] let r = s.[1..] (Array.exists ((=)c) colNames) && (Array.exists ((=)r) rowNames) let dependencies = Dependencies.Graph() let formulas = Dictionary<_, Expression>() let values = Dictionary() let rawValues = Dictionary() let setError cell text = values.[cell] <- EvalResult.Error text let getValue reference = match values.TryGetValue reference with | true, v -> v | _ -> EvalResult.Success 0.0 let deleteValue reference = values.Remove(reference) |> ignore let deleteFormula cell = match formulas.TryGetValue cell with | true, expr -> dependencies.Delete(cell, expr.GetReferences()) formulas.Remove(cell) |> ignore | _ -> () let evaluate cell = let deps = dependencies.GetDependents cell for d in deps do match formulas.TryGetValue d with | true, e -> let r = Operations.eval getValue e values.[d] <- r | _ -> () deps let setFormula cell text = let setError msg = setError cell msg [cell] :> seq<_> try match Parser.parse text with | Some expr -> let references = expr.GetReferences() let invalidReferences = [for r in references do if not (isValidReference r) then yield r] if not (List.isEmpty invalidReferences) then let msg = sprintf "Formula contains invalid references:%s" (String.concat ", " invalidReferences) setError msg else try dependencies.Insert(cell, references) formulas.Add(cell, expr) |> ignore evaluate cell with e -> setError e.Message | _ -> setError "Invalid formula text" with e -> setError e.Message member this.Headers = colNames member this.Rows = rowNames member this.GetRowReferences() = seq { for r in rowNames do let cells = [| for c in colNames do yield c + r |] yield { Name = r; Cells = cells } } member this.SetValue(cellRef : Reference, value : string) : Cell[] = rawValues.Remove(cellRef) |> ignore if not (String.IsNullOrEmpty value) then rawValues.[cellRef] <- value deleteFormula cellRef let affectedCells = if (value <> null && value.StartsWith "=") then setFormula cellRef value elif String.IsNullOrEmpty value then deleteValue cellRef evaluate cellRef else match Double.TryParse value with | true, value -> values.[cellRef] <- EvalResult.Success value evaluate cellRef | _ -> values.[cellRef] <- EvalResult.Error "Number expected" [cellRef] :> _ [| for r in affectedCells do let rawValue = match rawValues.TryGetValue r with | true, v -> v | false, _ -> "" let valueStr, hasErr = match values.TryGetValue r with | true, (EvalResult.Success v) -> (string v), false | true, (EvalResult.Error msg) -> msg, true | false, _ -> "", false let c = {Reference = r; Value = valueStr; RawValue = rawValue; HasError = hasErr} yield c |]
HOW TO:建立使用 F# 可攜式程式庫的 Silverlight 應用程式
在功能表列上,依序選擇 [檔案]、[加入] 和 [新增專案]。在 [加入新的專案] 對話方塊中,依序展開 [Visual C#]、[Silverlight],然後選擇 [Silverlight 應用程式]。[新 Silverlight 應用程式] 對話方塊隨即出現。
確定已選取 [在新網站中裝載 Silverlight 應用程式] 核取方塊,並在下拉式清單中確定已選取 [ASP.NET Web 應用程式專案],然後選擇 [確定] 按鈕。隨即會建立兩個專案:一個專案有 Silverlight 控制項,而另一個專案是裝載控制項的 ASP.NET Web 應用程式。
將參考加入至 Spreadsheet 專案。開啟 Silverlight 專案的 [參考] 節點的捷徑功能表,然後選擇 [加入參考]。[參考管理員] 隨即出現。展開 [方案] 節點,選擇 [Spreadsheet] 專案,然後選擇 [確定] 按鈕。
在這個步驟中,您會建立檢視模型,描述每個 UI 必須執行的所有事項,但不描述其如何出現。開啟專案節點的捷徑功能表,選擇 [加入],然後選擇 [新項目]。加入程式碼檔案,命名為 ViewModel.cs,然後將下列程式碼貼入其中:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using Portable.Samples.Spreadsheet; namespace SilverlightFrontEnd { public class SpreadsheetViewModel { private Spreadsheet spreadsheet; private Dictionary<string, CellViewModel> cells = new Dictionary<string, CellViewModel>(); public List<RowViewModel> Rows { get; private set; } public List<string> Headers { get; private set; } public string SourceCode { get { return @" type Spreadsheet(height : int, width : int) = do if height <= 0 then failwith ""Height should be greater than zero"" if width <= 0 || width > 26 then failwith ""Width should be greater than zero and lesser than 26"" let rowNames = [| for i = 0 to height - 1 do yield string (i + 1)|] let colNames = [| for i = 0 to (width - 1) do yield string (char (int 'A' + i)) |] let isValidReference (s : string) = if s.Length < 2 then false else let c = s.[0..0] let r = s.[1..] (Array.exists ((=)c) colNames) && (Array.exists ((=)r) rowNames) let dependencies = Dependencies.Graph() let formulas = Dictionary<_, Expression>() let values = Dictionary() let rawValues = Dictionary() let setError cell text = values.[cell] <- EvalResult.E text let getValue reference = match values.TryGetValue reference with | true, v -> v | _ -> EvalResult.S 0.0 let deleteValue reference = values.Remove(reference) |> ignore let deleteFormula cell = match formulas.TryGetValue cell with | true, expr -> dependencies.Delete(cell, expr.GetReferences()) formulas.Remove(cell) |> ignore | _ -> () "; } } public SpreadsheetViewModel(Spreadsheet spreadsheet) { this.spreadsheet = spreadsheet; Rows = new List<RowViewModel>(); foreach (var rowRef in spreadsheet.GetRowReferences()) { var rowvm = new RowViewModel { Index = rowRef.Name, Cells = new List<CellViewModel>() }; foreach (var reference in rowRef.Cells) { var cell = new CellViewModel(this, reference); cells.Add(reference, cell); rowvm.Cells.Add(cell); } Rows.Add(rowvm); } Headers = new[] { " " }.Concat(spreadsheet.Headers).ToList(); } public void SetCellValue(string reference, string newText) { var affectedCells = spreadsheet.SetValue(reference, newText); foreach (var cell in affectedCells) { var cellVm = cells[cell.Reference]; cellVm.RawValue = cell.RawValue; if (cell.HasError) { cellVm.Value = "#ERROR"; cellVm.Tooltip = cell.Value; // will contain error } else { cellVm.Value = cell.Value; cellVm.Tooltip = cell.RawValue; } } } } public class RowViewModel { public string Index { get; set; } public List<CellViewModel> Cells { get; set; } } public class CellViewModel : INotifyPropertyChanged { private SpreadsheetViewModel spreadsheet; private string rawValue; private string value; private string reference; private string tooltip; public CellViewModel(SpreadsheetViewModel spreadsheet, string reference) { this.spreadsheet = spreadsheet; this.reference = reference; } public string RawValue { get { return rawValue; } set { var changed = rawValue != value; rawValue = value; if (changed) RaisePropertyChanged("RawValue"); } } public string Value { get { return value; } set { var changed = this.value != value; this.value = value; if (changed) RaisePropertyChanged("Value"); } } public string Tooltip { get { return tooltip; } set { var changed = this.tooltip != value; this.tooltip = value; if (changed) { RaisePropertyChanged("Tooltip"); RaisePropertyChanged("TooltipVisibility"); } } } public Visibility TooltipVisibility { get { return string.IsNullOrEmpty(tooltip) ? Visibility.Collapsed : Visibility.Visible; } } public event PropertyChangedEventHandler PropertyChanged = delegate { }; public void SetCellValue(string newValue) { spreadsheet.SetCellValue(reference, newValue); } private void RaisePropertyChanged(string name) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } }
在 Silverlight 控制項專案中,開啟宣告主要試算表 UI 配置的 MainPage.xaml。在 MainPage.xaml 中,將下列 XAML 程式碼貼入至現有的格線項目。
<TextBlock Text="{Binding SourceCode}" FontSize="20" FontFamily="Consolas" Foreground="LightGray"/> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <StackPanel.Resources> <Style x:Key="CellBorder" TargetType="Border"> <Setter Property="BorderThickness" Value="0.5"/> <Setter Property="BorderBrush" Value="LightGray"/> </Style> <Style x:Key="CaptionBorder" TargetType="Border" BasedOn="{StaticResource CellBorder}"> <Setter Property="Background" Value="LightBlue"/> </Style> <Style x:Key="TextContainer" TargetType="TextBlock"> <Setter Property="FontSize" Value="26"/> <Setter Property="FontFamily" Value="Segoe UI"/> <Setter Property="Width" Value="200"/> <Setter Property="Height" Value="60"/> </Style> <Style x:Key="CaptionText" TargetType="TextBlock" BasedOn="{StaticResource TextContainer}"> <Setter Property="TextAlignment" Value="Center"/> <Setter Property="Foreground" Value="DimGray"/> </Style> <Style x:Key="ValueEditor" TargetType="TextBox"> <Setter Property="Width" Value="200"/> <Setter Property="Height" Value="60"/> <Setter Property="FontSize" Value="26"/> <Setter Property="FontFamily" Value="Segoe UI"/> </Style> <Style x:Key="ValueText" TargetType="TextBlock" BasedOn="{StaticResource TextContainer}"> <Setter Property="TextAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="Foreground" Value="Black"/> </Style> </StackPanel.Resources> <Border Style="{StaticResource CellBorder}"> <StackPanel> <ItemsControl ItemsSource="{Binding Headers}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Border Style="{StaticResource CaptionBorder}"> <TextBlock Text="{Binding}" Style="{StaticResource CaptionText}"/> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <ItemsControl ItemsSource="{Binding Rows}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Border Style="{StaticResource CaptionBorder}"> <TextBlock Text="{Binding Index}" Style="{StaticResource CaptionText}"/> </Border> <ItemsControl ItemsSource="{Binding Cells}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Border Style="{StaticResource CellBorder}"> <Grid> <TextBox Name="editor" Tag="{Binding ElementName=textContainer}" Visibility="Collapsed" LostFocus="OnLostFocus" KeyUp="OnKeyUp" Text ="{Binding RawValue}" Style="{StaticResource ValueEditor}"/> <TextBlock Name="textContainer" Tag="{Binding ElementName=editor}" Visibility="Visible" Text="{Binding Value}" Style="{StaticResource ValueText}" MouseLeftButtonDown="OnMouseLeftButtonDown" ToolTipService.Placement="Mouse"> <ToolTipService.ToolTip> <ToolTip Visibility="{Binding TooltipVisibility}"> <TextBlock Text="{Binding Tooltip}" Style="{StaticResource TextContainer}" Visibility="{Binding TooltipVisibility}"/> </ToolTip> </ToolTipService.ToolTip> </TextBlock> </Grid> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </StackPanel> </Border> </StackPanel>
在 MainPage.xaml.cs 中,將 using SilverlightFrontEnd; 加入至 using 指示詞清單,然後將下列方法加入至 SilverlightApplication1 類別。
void OnLostFocus(object sender, RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); } void OnKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) { HideEditor(e); e.Handled = true; return; } else if (e.Key == Key.Enter) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); e.Handled = true; } } private void EditValue(object dataContext, string newText) { var cvm = (CellViewModel)dataContext; cvm.SetCellValue(newText); } private void OnMouseLeftButtonDown(object sender, RoutedEventArgs e) { var textBlock = (TextBlock)e.OriginalSource; var editor = (TextBox)textBlock.Tag; textBlock.Visibility = Visibility.Collapsed; editor.Visibility = Visibility.Visible; editor.Focus(); } private void HideEditor(RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var textBlock = (TextBlock)editor.Tag; editor.Visibility = Visibility.Collapsed; textBlock.Visibility = Visibility.Visible; }
在 App.xaml.cs 中,將下列 using 指示詞加入:
using SilverlightFrontEnd; using Portable.Samples.Spreadsheet;
將下列程式碼貼至 Application_Startup 事件處理常式:
var spreadsheet = new Spreadsheet(5, 5); var spreadsheetViewModel = new SpreadsheetViewModel(spreadsheet); var main = new MainPage(); main.DataContext = spreadsheetViewModel; this.RootVisual = main;
您可以直接啟動 Silverlight 專案,或是啟動裝載 Silverlight 控制項的 ASP.NET Web 應用程式,來測試 Silverlight 前端。開啟其中一個專案的節點的捷徑功能表,然後選擇 [設定為啟始專案]。
HOW TO:建立使用 F# 可攜式程式庫的 Windows 市集 應用程式
在本節中,您將會建立使用 F# 試算表程式碼做為計算元件的 Windows 市集 應用程式。在功能表列上選擇 [檔案]、[加入]、[新的專案]。[新增專案] 對話方塊隨即出現。在 [已安裝] 底下,依序展開 [Visual C#]、[Windows 市集],然後選擇 [空白應用程式] 範本。將專案命名為 NewFrontEnd,然後選擇 [確定] 按鈕。如果提示您輸入程式開發人員授權以 Windows 市集 應用程式,請輸入認證。如果您沒有認證,可以在這裡找到設定認證的方法。
專案隨即建立。注意這個專案的組態設定和內容。預設 [參考] 會包含適用於 Windows 市集應用程式的 .NET (這是與 Windows 市集 應用程式相容之 .NET Framework 的子集) 和 Windows 組件 (包括 Windows 執行階段應用程式開發介面與 Windows 市集 應用程式的 UI)。Assets 和 Common 子資料夾已建立。Assets 子資料夾包含數個適用於 Windows 市集 應用程式的圖示,而 Common 子資料夾包含 Windows 市集 應用程式範本使用的共用常式。預設專案範本也會建立 App.xaml、BlankPage.xaml 及其關聯的 C# 程式碼後置檔案、App.xaml.cs 與 BlankPage.xaml.cs。App.xaml 描述整個應用程式,而 BlankPage.xaml 則描述其已定義的一個 UI 介面。最後,所有 .pfx 檔和 .appxmanifest 檔都會支援 Windows 市集 應用程式的安全性和部署模型。
開啟 Silverlight 專案的 [參考] 節點的捷徑功能表並選擇 [加入參考],將參考加入至 Spreadsheet 專案。在 [參考管理員] 中,展開 [方案] 節點,選擇 [Spreadsheet] 專案,然後選擇 [確定] 按鈕。
您將需要您已經在 Silverlight 專案中使用的部分程式碼,以支援 Windows 市集 應用程式的 UI 程式碼。這個程式碼在 ViewModels.cs 中。開啟 [NewFrontEnd] 專案節點的捷徑功能表,選擇 [加入],然後選擇 [新項目]。加入 C# 程式碼檔,並將其命名為 ViewModels.cs。將 Silverlight 專案中 ViewModels.cs 的程式碼貼上,然後變更這個檔案最上方的 using 指示詞區塊。移除 System.Windows (這是 Silverlight UI 使用的),並加入 Windows.UI.Xaml 和 Windows.Foundation.Collections (用於 Windows 市集 應用程式的 UI)。Silverlight 和 Windows 市集 的 UI 都是以 WPF 為基礎,因此彼此相容。更新後的 using 指示詞區塊應類似下列範例:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Input; using Portable.Samples.Spreadsheet; using Windows.Foundation.Collections; using Windows.UI.Xaml;
此外,將 ViewModels.cs 中的命名空間從 SilverlightFrontEnd 變更為 NewFrontEnd。
您可以重複使用 ViewModels.cs 中其餘的程式碼,但有些型別 (例如 Visibility) 目前為適用於 Windows 市集 應用程式而非 Silverlight 的版本。
在這個 Windows 市集 個應用程式中,App.xaml.cs 程式碼檔的啟始程式碼必須與出現在 Silverlight 應用程式之 Application_Startup 事件處理常式中的相類似。在 Windows 市集 應用程式中,這個程式碼會出現於 App 類別的 OnLaunched 事件處理常式中。將下列程式碼加入至 App.xaml.cs 中的 OnLaunched 事件處理常式。
var spreadsheet = new Spreadsheet(5, 5); var spreadsheetViewModel = new SpreadSheetViewModel(spreadsheet);
加入 Spreadsheet 程式碼的 using 指示詞。
using Portable.Samples.Spreadsheet;
在 App.xaml.cs 中,OnLaunched 包含會指定載入哪個頁面的程式碼。您將會加入您想要在使用者啟動應用程式時載入的頁面。變更 OnLaunched 中的程式碼以巡覽至第一頁,如下列範例所示:
// Create a frame, and navigate to the first page. var rootFrame = new Frame(); rootFrame.Navigate(typeof(ItemsPage1), spreadsheetViewModel);
因為這個範例沒有使用 BlankPage1.xaml 及其程式碼後置檔案,您可以刪除這些檔案。
開啟 [NewFrontEnd] 專案節點的捷徑功能表,選擇 [加入],然後選擇 [新項目]。加入項目頁面,並保留預設名稱 ItemsPage1.xaml。這個步驟同時會將 ItemsPage1.xaml 及其程式碼後置檔案 ItemsPage1.xaml.cs 加入至專案。ItemsPage1.xaml 開頭為主要的 common:LayoutAwarePage 標記,其中包含許多屬性,如下列 XAML 程式碼所示:
<common:LayoutAwarePage x:Name="pageRoot" x:Class="NewFrontEnd.ItemsPage1" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:NewFrontEnd" xmlns:common="using:NewFrontEnd.Common" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
Windows 市集 應用程式 UI 與您建立的 Silverlight 應用程式 UI 相同,而 XAML 格式,在這個案例中是相同的。因此,Windows 市集 應用程式 UI 的 ItemsPage1.xaml 可以重複使用 Silverlight 專案之 MainPage.xaml 中的 XAML。
在 Silverlight 專案中複製 MainPage.xaml 最上層格線項目的程式碼,並在 Windows 市集 應用程式 UI 的專案中將其貼入 ItemsPage1.xaml 的最上層格線項目。貼入程式碼時,您可以覆寫格線項目的任何現有內容。將格線項目上的 Background 屬性變更為 "White",並將 MouseLeftButtonDown 取代成 PointerPressed。
這個事件的名稱在 Silverlight 應用程式和 Windows 市集 應用程式中各不相同。
在 ItemsPage.xaml.cs 中,藉由變更OnNavigatedTo 方法來設定 DataContext 屬性。
protected override void OnNavigatedTo(NavigationEventArgs e) { this.DataContext = e.Parameter; }
複製下列事件處理常式程式碼,並將其貼入 ItemsPage1 類別:OnLostFocus、OnKeyUp、EditValue、OnPointerPressed 和 HideEditor。
void OnLostFocus(object sender, RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); } void OnKeyUp(object sender, KeyEventArgs e) { if (e.Key == Windows.System.VirtualKey.Escape) { HideEditor(e); e.Handled = true; return; } else if (e.Key == Windows.System.VirtualKey.Enter) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); e.Handled = true; } } private void EditValue(object dataContext, string newText) { var cvm = (CellViewModel)dataContext; cvm.SetCellValue(newText); } private void OnPointerPressed(object sender, RoutedEventArgs e) { var textBlock = (TextBlock)e.OriginalSource; var editor = (TextBox)textBlock.Tag; textBlock.Visibility = Windows.UI.Xaml.Visibility.Collapsed; editor.Visibility = Windows.UI.Xaml.Visibility.Visible; editor.Focus(FocusState.Programmatic); } private void HideEditor(RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var textBlock = (TextBlock)editor.Tag; editor.Visibility = Windows.UI.Xaml.Visibility.Collapsed; textBlock.Visibility = Windows.UI.Xaml.Visibility.Visible; }
將啟始專案變更為您的 Windows 市集 應用程式專案。開啟 [NewFrontEnd] 專案節點的捷徑功能表,選擇 [設定為啟始專案],然後選擇 F5 鍵執行專案。
建立使用 F# 的 C# 可攜式程式庫
前一個範例會複製範例程式碼是因為 ViewModels.cs 程式碼出現在多個專案中。在本節中,您會建立 C# 可攜式程式庫來包含這個程式碼。在某些情況下,當應用程式取用了使用 F# 的可攜式程式庫時,您必須將資訊加入至應用程式的組態檔。此時,目標為 .NET Framework 4.5 桌面版本的桌面應用程式會參考 C# 可攜式程式庫,而這又接著參考 F# 可攜式程式庫。如此一來,您就必須將繫結重新導向加入至主應用程式的 app.config 檔案。您必須加入這個重新導向,因為載入的 FSharp.Core 程式庫只有一個版本,但是可攜式程式庫會參考 .NET 可攜式版本。所有對 FSharp.Core 函式的 .NET 可攜式版本的呼叫都必須重新導向至 FSharp.Core 在桌面應用程式中載入的那個單一版本。由於 Silverlight 5 和 Windows 市集 應用程式的執行階段環境使用的是 FSharp.Core 的 .NET 可攜式版本,而非完整的桌面版本,因此只有桌面應用程式才需要繫結重新導向。
HOW TO:建立參考使用 F# 之可攜式程式庫的桌面應用程式
在功能表列上選擇 [檔案]、[加入]、[新的專案]。在 [已安裝] 底下,展開 [Visual C#] 節點,選擇 [.NET 可攜式程式庫] 專案範本,然後將專案命名為 ViewModels。
您必須將這個 .NET 可攜式程式庫的目標設定成可與您要加入參考的 F# 可攜式程式庫相符。否則,錯誤訊息會告知您不相符。在 ViewModels 專案的捷徑功能表上,選擇 [屬性]。在 [程式庫] 索引標籤上,將這個可攜式類別庫的目標設定成可與 .NET Framework 4.5、Silverlight 5 和 Windows 市集 應用程式相符。
在 [參考] 節點的捷徑功能表中,選擇 [加入參考]。在 [方案] 底下,選取 [Spreadsheet] 旁邊的核取方塊。
從其中一個其他專案複製 ViewModels.cs 的程式碼,並將其貼入 ViewModels 專案的程式碼檔。
進行下列變更,這會讓 ViewModels 中的程式碼完全不受 UI 平台的侷限:
移除 System.Windows、System.Windows.Input、Windows.Foundation.Collections 和 Windows.UI.Xaml 的 using 指示詞 (如果有的話)。
將命名空間變更為 ViewModels。
移除 TooltipVisibility 屬性。這個屬性會使用 Visibility,這是平台相依物件。
在功能表列上選擇 [檔案]、[加入]、[新的專案]。在 [已安裝] 底下,展開 [Visual C#] 節點,然後選擇 [WPF 應用程式] 專案範本。將新專案命名為 Desktop,並選擇 [確定] 按鈕。
開啟 [Desktop] 專案中 [參考] 節點的捷徑功能表,然後選擇 [加入參考]。在 [方案] 底下,選擇 [Spreadsheet] 和 [ViewModels] 專案。
開啟 WPF 應用程式的 app.config 檔,然後加入下列程式碼行:這個程式碼會設定適當的繫結重新導向,以便在目標為 .NET Framework 4.5 的桌面應用程式參考使用 F# 的 .NET 可攜式程式庫時套用。.NET 可攜式程式庫會使用 2.3.5.0 版的 FSharp.Core 程式庫,而 .NET Framework 4.5 桌面應用程式會使用 4.3.0.0 版。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="FSharp.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/> <bindingRedirect oldVersion="2.3.5.0" newVersion="4.3.0.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
現在您必須將參考加入至 F# 核心程式庫的可攜式版本。每當您的應用程式取用參考 F# 可攜式程式庫的可攜式程式庫時,就會需要這個參考。
開啟 [Desktop] 專案中 [參考] 節點的捷徑功能表,然後選擇 [加入參考]。選擇 [瀏覽],然後巡覽至已安裝 Visual Studio 之 Program Files 資料夾底下的 Reference Assemblies\Microsoft\FSharp\3.0\Runtime\.NETPortable\FSharp.Core.dll。
在 Desktop 專案中,將 ViewModels.cs 及 Portable.Samples.Spreadsheet 的 using 指示詞加入至 App.xaml.cs 和 MainWindow.xaml.cs。
using ViewModels; using Portable.Samples.Spreadsheet;
開啟 MainWindow.xaml 檔案,然後將視窗類別的標題屬性變更為 Spreadsheet。
在 Silverlight 專案中複製 MainPage.xaml 之格線項目內的程式碼,並在 Desktop 專案中將該程式碼貼入 MainWindow.xaml 的格線項目。
從 Silverlight 專案中複製 MainPage.xaml.cs 的事件處理程式碼,並將該程式碼貼入 Desktop 專案中的 MainWindow.xaml.cs。
void OnLostFocus(object sender, RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); } void OnKeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) { HideEditor(e); e.Handled = true; return; } else if (e.Key == Key.Enter) { var editor = (TextBox)e.OriginalSource; var text = editor.Text; HideEditor(e); EditValue(editor.DataContext, text); e.Handled = true; } } private void EditValue(object dataContext, string newText) { var cvm = (CellViewModel)dataContext; cvm.SetCellValue(newText); } private void OnMouseLeftButtonDown(object sender, RoutedEventArgs e) { var textBlock = (TextBlock)e.OriginalSource; var editor = (TextBox)textBlock.Tag; textBlock.Visibility = Visibility.Collapsed; editor.Visibility = Visibility.Visible; editor.Focus(); } private void HideEditor(RoutedEventArgs e) { var editor = (TextBox)e.OriginalSource; var textBlock = (TextBlock)editor.Tag; editor.Visibility = Visibility.Collapsed; textBlock.Visibility = Visibility.Visible; }
將試算表啟始程式碼加入至 MainWindow.xaml.cs 中的 MainWindow 建構函式,並將 MainPage 的參考取代成 MainWindow 的參考。
public MainWindow() { var spreadsheet = new Spreadsheet(5, 5); var spreadsheetViewModel = new SpreadsheetViewModel(spreadsheet); this.DataContext = spreadsheetViewModel; InitializeComponent(); }
開啟 Desktop 專案的捷徑功能表,然後選擇 [設定為啟始專案]。
選擇 F5 鍵以建置應用程式,然後進行偵錯。
後續步驟
或者,您可以修改 Windows 市集 應用程式與 Silverlight 應用程式的專案,以使用新的 ViewModels 可攜式程式庫。
繼續了解 Windows 市集 應用程式,請移至 Windows 開發人員中心。