Dzisiaj wrócę do tematu użycia języka C# jako języka skryptowego przy pomocy Mono.CSharp.dll i opiszę w jaki sposób przekazać parametry do takiego skryptu. Pominę podejście opierające się o wklejanie do skryptu string'owej reprezentacji takich parametrów i od razu przejdę do bardziej eleganckiego rozwiązania. Bazuje ono na tym co przeczytałem w tym poście.
Zaczynamy od utworzenia statycznej klasy ScriptContext, która posłuży nam do wymiany danych pomiędzy skryptem, a naszym programem.
Zaczynamy od utworzenia statycznej klasy ScriptContext, która posłuży nam do wymiany danych pomiędzy skryptem, a naszym programem.
namespace Scripting { public static class ScriptContext { static ScriptContext() { Items = new Dictionary<string, object>(); } public static object Result { get; set; } public static IDictionary<string, object> Items { get; set; } } }
Użycie jej nie jest trudne. Mały przykład:
var settings = new CompilerSettings(); var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter())); //Informujemy silnik skryptowy gdzie została zdefiniowana klasa ScriptContext ev.ReferenceAssembly(typeof(ScriptContext).Assembly); //Ustawiamy parametry ScriptContext.Items["param1"] = new List<string> { "Hello!", "Welcome!", "Hi!" }; ev.Run("using System;"); ev.Run("using Scripting;"); //Korzystamy z przekazanych parametrów ev.Run("foreach(var s in (System.Collections.Generic.IEnumerable<string>)ScriptContext.Items[\"param1\"]) Console.WriteLine(s);");
Właściwość ScriptContext.Result dodałem aby nie musieć zastanawiać się kiedy wywołać metodę Evaluator.Run, a kiedy Evaluator.Evaluate. Zawsze używam tej pierwszej, zakładając, że wynik ze skryptu zwracany jest przy użyciu tej właściwości np.:
ev.Run("ScriptContext.Result = 1;"); Console.WriteLine(ScriptContext.Result);
Do szczęścia brakowało mi jednak jeszcze jednej rzeczy. Jak pokazuje pierwszy przykład musiałem użyć rzutowania do IEnumerable<string> aby cieszyć się silnie typowanym parametrem. Napisałem więc metody pomocnicze, które przygotowują silnie typowane parametry:
public static class ScriptingHelper { public static StringBuilder PrepareParameters(IDictionary<string, object> parameters) { ScriptContext.Items = parameters; var sb = new StringBuilder(); if (parameters != null) { foreach (var kvp in parameters) { var typeName = ExtractTypeName(kvp); sb.AppendFormat("var {0} = ({1}){2}.Items[\"{0}\"];", kvp.Key, typeName, typeof(ScriptContext).Name); sb.AppendLine(); } } return sb; } private static string ExtractTypeName(KeyValuePair<string, object> kvp) { if (kvp.Value == null) throw new Exception("null parameters are not supported!"); var type = kvp.Value.GetType(); using (var provider = new CSharpCodeProvider()) { var typeRef = new CodeTypeReference(type); return provider.GetTypeOutput(typeRef); } } }
Nic bardzo skomplikowanego ale warto zwrócić uwagę na użycie klasy CSharpCodeProvider. Dzięki niej uzyskuję poprawną, pełną nazwę typów, również tych generycznych. Gdybym na przykład dla listy int'ów spróbował użyć czegoś w rodzaju list.GetType().Name to w wyniku otrzymałbym List`1, co do rzutowania się nie nadaje. Wcześniejszy przykład można więc napisać teraz w taki sposób:
//Ustawiamy parametry var parameters = ScriptingHelper.PrepareParameters(new Dictionary<string, object> { {"param1", new <string> { "Hello!", "Welcome!", "Hi!" } } }); ev.Run("using System;"); ev.Run("using Scripting;"); ev.Run(parameters.ToString()); //Korzystamy z przekazanych parametrów ale już bez rzutowania ev.Run("foreach(var s in param1) Console.WriteLine(s);");
Wracając do mojej aplikacji, w której chciałem umożliwić użytkownikowi definiowanie własnych algorytmów obliczania odległości między wektorami. Dane wejściowe to oczywiście dwa wektory. Dzięki takiemu podejściu, zamiast zmuszać użytkownika to rzutowania parametrów na określony typ, używam konwencji czyli: wektor numer 1 znajduje się w zmiennej vector1 itd.
Klasa ScriptingHelper jest również doskonałym miejsce na dodawanie różnych innych "przydasiów" ułatwiających pracę użytkownikowi.