Tuesday, December 19, 2006

Running Action Script under WPF

Can we run Adobe ActionScript from WPF? YES! we can. Just look at this screenshot

Nice, let's see how we do it. First let's take out  our textbox with syntax hightlighing and use it within the project. We not really do it with ActionScript, but with MS JScript (which is seemed to be almost compatible to AS2.0).

So the next step (after we wrote JS) is to compile it. We'll use System.Reflection and System.Reflection.Emit namespaces. Let's take our source and compile it

public static CompilerResults Compile(string source)
        {
 
            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateInMemory = true;
 
            CompilerResults results = (new Microsoft.JScript.JScriptCodeProvider()).CompileAssemblyFromSource(parameters, source);

Fine, now we have results. If the result has errors we do not want to process so

if (results.Errors.HasErrors)
            {
                return results;
            }

In order to make the use know about errors, we'll build panel like in Visual Studio with all of our exceptions (if there are). We'll make XMLDataProvider resource for error list

<XmlDataProvider x:Key="myDebugData" XPath="/myXmlData">
      <x:XData>
        <myXmlData xmlns=""/>
      </x:XData>
    </XmlDataProvider>

and add there all of our erorrs


lstErrors.SetValue(ListView.HeightProperty, (double)0);
            errors = Resources["myDebugData"] as XmlDataProvider;
            errors.Document.SelectSingleNode(errors.XPath).RemoveAll();
 
            System.CodeDom.Compiler.CompilerResults results = JSCompiler.Compile(source);
            if (results.Errors.HasErrors)
            {
                WriteErrors(results.Errors);
                return;
            }
            //no errors. Show methods
            myMethods = Resources["myMethods"] as JSMethods;
            myMethods.Add(new JSMethod(JSCompiler.LastDelegate));

 


As you can see, I change Height dependency property of ListView in order to recieve the behaviour of VS error panel. This ListView is binded to our data source, so we do not need anything except it

<ListView DockPanel.Dock="Bottom" Height="0" Name="lstErrors" ItemsSource="{Binding Source={StaticResource myDebugData}, XPath=Error}">
      <ListView.View>
        <GridView x:Name="debugView">
          <GridViewColumn Header="Description" Width="300" DisplayMemberBinding="{Binding XPath=@Description}"/>
          <GridViewColumn Header="File" Width="120" DisplayMemberBinding="{Binding XPath=@File}"/>
          <GridViewColumn Header="Line" DisplayMemberBinding="{Binding XPath=@Line}"/>
          <GridViewColumn Header="Column" DisplayMemberBinding="{Binding XPath=@Column}"/>
          <GridViewColumn Header="Project" DisplayMemberBinding="{Binding XPath=@Project}"/>
        </GridView>
      </ListView.View>
    </ListView>

 


Let's make it fun!


void ShowErrors()
        {
            DoubleAnimation errAni = new DoubleAnimation(0, this.ActualHeight / 6, new Duration(TimeSpan.FromSeconds(0.5)));
            Storyboard.SetTargetName(errAni, "lstErrors");
            Storyboard.SetTargetProperty(errAni, new PropertyPath(ListView.HeightProperty));
            Storyboard errStory = new Storyboard();
            errStory.Children.Add(errAni);
            errStory.Begin(this);
        }

And usefull

void WriteError(System.CodeDom.Compiler.CompilerError error)
        {
            XmlElement item = errors.Document.CreateElement("Error");
            item.SetAttribute("Description", error.ErrorText);
            item.SetAttribute("File", error.FileName);
            item.SetAttribute("Line", error.Line.ToString());
            item.SetAttribute("Column", error.Column.ToString());
            item.SetAttribute("Project", currentFileName);
 
            errors.Document.SelectSingleNode(errors.XPath).AppendChild(item);
        }

 


If we have no errors, let's  process to our code compilation.


else
{
    Assembly result = results.CompiledAssembly;
    Type[] resultTypes = result.GetTypes();
    for (int j = 0; j < resultTypes.Length; j++)
    {
        if (resultTypes[j].BaseType != typeof(Microsoft.JScript.GlobalScope))
        {
            MethodInfo[] mi = resultTypes[j].GetMethods();
            for (int i = 0; i < mi.Length; i++)
            {
                //we do not want default object's methods
                if (mi[i].Name != "GetType" &
                    mi[i].Name != "ToString" &
                    mi[i].Name != "Equals" &
                    mi[i].Name != "GetHashCode")
                {
                    Type delegateType = CreateDelegate(mi[i]);
 
                    object instance = Activator.CreateInstance(resultTypes[j]);
 
                    if ((mi[i].Attributes & MethodAttributes.Static) == MethodAttributes.Static)
                    {
                        instance = null;
                    }
                    else
                    {
                        instance = Activator.CreateInstance(resultTypes[j]);
                    }
 
                    LastDelegate = Delegate.CreateDelegate(delegateType, instance, mi[i]);
 
                }
            }
        }
    }

We do not really want to compile our code into assemby and reference it to our binaries, so we'll have to create a list of delegates in order to be able to execute those IL methods. The tricky part is to know if the method, I want to create delegate for is not static, 'cos in this case we wont create an instance of hosing application. So let's create a delegate type from MethodInfo for our method.


static Type CreateDelegate(MethodInfo method)
        {
            AssemblyName assembly = Assembly.GetExecutingAssembly().GetName();
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assembly, AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(method.Module.Name);
            TypeBuilder typeBuilder = moduleBuilder.DefineType(method.Name, TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, typeof(System.MulticastDelegate));
            ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { method.ReturnType });
            constructorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);
 
            ParameterInfo[] parameters = method.GetParameters();
            Type[] parameterTypes = new Type[parameters.Length];
 
            for (int i = 0; i < parameters.Length; i++)
            {
                parameterTypes[i] = parameters[i].ParameterType;
            }
 
            MethodBuilder methodBuilder = typeBuilder.DefineMethod("Invoke", MethodAttributes.PrivateScope | MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.VtableLayoutMask, method.ReturnType, parameterTypes);
            methodBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed | method.GetMethodImplementationFlags());
 
            Type type = typeBuilder.CreateType();
 
            return type;
        }

Fine, now the only thing we should do is create the instance of this delegate and execute it when needed. In order to make me able to bind and manage it and later use it for automatic executions and WPF code generation. I will want to compare those methods in order to know if the method I just created is not the method I already have. So overroding Method operators will really help me with it.


public class JSMethod
    {
        private Delegate executer;
        public Delegate Executer
        {
            get { return executer; }
            set { executer = value; }
        }
 
        private string methodName;
        public string MethodName
        {
            get { return methodName; }
            set { methodName = value; }
        }
 
        private JSCompiler.Parameter[] parameters;
        public JSCompiler.Parameter[] Parameters
        {
            get { return parameters; }
            set { parameters = value; }
        }
 
        private string className;
        public string ClassName
        {
            get { return className; }
            set { className = value; }
        }
        public JSMethod(Delegate source)
        {
            this.executer = source;
            if (this.executer.Target != null)
            {
                this.className = this.executer.Target.GetType().ToString();
            }
            else {
                this.className = "Static Class";
            }
            parameters = JSCompiler.GetParameters(source);
            string sps = "";
            for (int i = 0; i < parameters.Length; i++)
            {
                sps += parameters[i].ToString();
            }
            this.methodName = this.executer.Method.Name+"( "+sps+" )";
        }
        public JSMethod(string cName)
        {
            className = cName;
        }
 
        public static bool operator ==(JSMethod method1, JSMethod method2)
        {
            object o1 = method1;
            object o2 = method2;
            if (o1 == null & o2 == null)
                return true;
            else if (o1 == null | o2 == null)
                return false;
            else if (method1.ClassName == method2.ClassName &&
           method1.MethodName == method2.MethodName)
                return true;
            return false;
        }
        public static bool operator !=(JSMethod method1, JSMethod method2)
        {
            return !(method1 == method2);
        }
 
    }

The ObservableCollection of all methods will be later binded to hierarcical tree view to group it by classes


public class JSMethods : ObservableCollection<JSMethod>
    {
        public JSMethods()
        {
 
        }
 
 
        public new void Add(JSMethod method)
        {
            for (int i = 0; i < base.Count; i++)
            {
                if (this[i] == method)
                    return;
            }
            base.Add(method);
        }
    }

Ok, now how to group it? If my data source was XML data or any other hierarcical data, I was able just bind it to treeview, and smart Binding will parse it as needed, but my data is not hierarcical, so I'll have to make some woodoo in order to "tell" control how to show it. First of all I need a source and this is really simple touch.

<local:JSMethods x:Key="myMethods"/>

If I want to bing it as is, I would use something like this


<ObjectDataProvider x:Key="myMethods" ObjectType="{x:Type local:JSMethods}"/>


But, because of my data is hierarcical, I'll need something like this

<CollectionViewSource x:Key="methodsViewSource" Source="{Binding Source={StaticResource myMethods}}">
      <CollectionViewSource.GroupDescriptions>
        <PropertyGroupDescription PropertyName="ClassName"/>
      </CollectionViewSource.GroupDescriptions>        
    </CollectionViewSource>

Even if I told my data how to group, it's not so clear for the control, so I'll need additional templates to use

<HierarchicalDataTemplate x:Key="classTemplate"
      ItemTemplate="{StaticResource methodTemplate}"
      ItemsSource="{Binding Path=Items}">
      <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
    </HierarchicalDataTemplate>

and one "regulat" template for items

<DataTemplate x:Key="methodTemplate" DataType="{x:Type local:JSMethod}">
      <ContentControl  MouseDoubleClick="onMethodClick">
        <TextBlock Text="{Binding Path=MethodName}"/>
      </ContentControl>
    </DataTemplate>

If I'd like to continue hierarcy, I'll need to use another Hierarchical data template for it, but in this case, what I have is prety enough for me. Bind 'em all


<TreeView Name="methodsTree" DataContext="{Binding Source={StaticResource methodsViewSource}}" ItemsSource="{Binding Path=Groups}" ItemTemplate="{StaticResource classTemplate}" Width="0">


That's all folks. I got a error list, delegates, bindings. Now I want to execute it. That mean, I'll have to build custom user interface, that will be built according my data types and all of my parameters for all methods. Do it.


While clicking on the method descriptor, I will exchange its content with new generated panel, incuding everything I know about the method and will pass it within right types into delegate and evaluate it


 


protected void onMethodClick(object sender, MouseButtonEventArgs e)
        {
            JSMethod method = methodsTree.SelectedItem as JSMethod;
            if (method == null)
                return;
            StackPanel sp = new StackPanel();
            sp.Width = this.Width - 50;
            sp.Height = method.Parameters.Length*40+50;
            sp.Orientation = Orientation.Vertical;
            for (int i = 0; i < method.Parameters.Length; i++)
            {
                StackPanel s = new StackPanel();
                s.Width = this.Width - 50;
                s.Height = 40;
                s.Orientation = Orientation.Horizontal;
                TextBox t = new TextBox();
                t.Height = 20;
                t.Width = this.Width / 2 - 50;
                t.Text = method.Parameters[i].Name+"("+method.Parameters[i].Type.ToString()+")";
 
                TextBox tb = new TextBox();
                tb.Height = 20;
                tb.Width = this.Width / 2 - 50;
                tb.Name=method.Parameters[i].Name;
 
                s.Children.Add(t);
                s.Children.Add(tb);
                sp.Children.Add(s);
            }
            Button b = new Button();
            b.VerticalAlignment = VerticalAlignment.Bottom;
            b.Click += new RoutedEventHandler(b_Click);
            b.Height = 50;
            b.Width = this.Width-50;
            b.Content = "Execute Method";
 

How, I have to return my content of what I had in this line after method execution. So I'll save it with tags

sp.Tag = method;
            sp.Children.Add(b);
 
            ContentControl cc = sender as ContentControl;
            object[] obs = {
                cc,
                cc.Content
            };
            b.Tag = obs;
            cc.Content = sp;

On click the execute button, I'll want to execute method and return all I had


void b_Click(object sender, RoutedEventArgs e)
        {
            Button b = sender as Button;
            StackPanel sp = b.Parent as StackPanel;
            if (sp != null)
            { 
                JSMethod method = sp.Tag as JSMethod;
                if(method != null)
                {
                    object[] obs = b.Tag as object[];
                    ContentControl cc = obs[0] as ContentControl;
                    sp = cc.Content as StackPanel;
 
                    for (int i = 0; i < method.Parameters.Length; i++)
                    { 
                        StackPanel ssp = sp.Children[i] as StackPanel;
                        if (ssp != null)
                        {
                            method.Parameters[i].Value = GetValueString(method.Parameters[i].Name, ssp, method.Parameters[i].Type);
                        }
                    }
                    object o = JSCompiler.ExecuteMethod(method);
 
                    MessageBox.Show("The result value of " + method.MethodName + " is " + o.ToString(), "Result value of method evaluation", MessageBoxButton.OK, MessageBoxImage.Information);
                    object obb = obs[1];
                    cc.Content = obb;
                }
            }
        }

 


The other thing I should do is to find what parametes the user entered. Using VisualTree it become very easy

object GetValueString(string elementName, Visual elementParent, Type elementType)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(elementParent); i++)
            {
                Visual v = (Visual)VisualTreeHelper.GetChild(elementParent, i);
                if(v is TextBox)
                {
                    TextBox t = v as TextBox;
                    if(t.Name == elementName)
                    {
                        return Convert.ChangeType(t.Text, elementType);
                    }
                }
            }
            return null;
        }

 


The final salute is just execute it. You don't believe me. It's just one line of code. Here it comes

public static object ExecuteMethod(Delegate source, out Type returnType, params Parameter[] parameters)
        {
            returnType = source.Method.ReturnType;
            object[] ps = new object[parameters.Length];
            for (int i = 0; i < parameters.Length; i++)
            {
                ps[i] = parameters[i].Value;
            }
            return source.DynamicInvoke(ps);
        }

That's really all now. Thank you reflection, emit and WPF to beat evil Adobe with their "Ultra dynamic and useful tools". We really dont need it. We can do everything ourself!


Source code for this article

No comments: