views:

385

answers:

2

Ok, here's the deal: I have code that works in C#, but when I call it from PowerShell, it fails. I can't quite figure it out, but it's something specific to PowerShell. Here's the relevant code calling the library (assuming you've added a reference ahead of time) from C#:

public class Test {
   [STAThread]
   public static void Main()
   {
      Console.WriteLine(  PoshWpf.XamlHelper.RoundTripXaml(
           "<TextBlock Text=\"{Binding FullName}\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"/&gt;"
      ) );
   }
}

Compiled into an executable, that works fine ... but if you call that method from PowerShell, it returns with no {Binding FullName} for the Text!

add-type -path .\PoshWpf.dll
[PoshWpf.Test]::Main()

I've pasted below the entire code for the library, all wrapped up in a PowerShell Add-Type call so you can just compile it by pasting it into PowerShell (you can leave off the first and last lines if you want to paste it into a new console app in Visual Studio.

To output (from PowerShell 2) as an executable, just change the -OutputType parameter to ConsoleApplication and the -OutputAssembly to PoshWpf.exe (or something). Thus, you can see that running the SAME CODE from the executable gives you the correct output.

But running the two lines as above or manually calling [PoshWpf.XamlHelper]::RoundTripXaml or [PoshWpf.XamlHelper]::ConvertToXaml from PowerShell just doesn't seem to work at all ... HELP?!

Add-Type -TypeDefinition @"

using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

namespace PoshWpf
{
    public class Test {
       [STAThread]
       public static void Main()
       {
          Console.WriteLine(  PoshWpf.XamlHelper.RoundTripXaml(
               "<TextBlock Text=\"{Binding FullName}\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"/&gt;"
          ) );
       }
    }

   public class BindingTypeDescriptionProvider : TypeDescriptionProvider
   {
      private static readonly TypeDescriptionProvider _DEFAULT_TYPE_PROVIDER = TypeDescriptor.GetProvider(typeof(Binding));

      public BindingTypeDescriptionProvider() : base(_DEFAULT_TYPE_PROVIDER) { }

      public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
      {
         ICustomTypeDescriptor defaultDescriptor = base.GetTypeDescriptor(objectType, instance);
         return instance == null ? defaultDescriptor : new BindingCustomTypeDescriptor(defaultDescriptor);
      }
   }

   public class BindingCustomTypeDescriptor : CustomTypeDescriptor
   {
      public BindingCustomTypeDescriptor(ICustomTypeDescriptor parent) : base(parent) { }

      public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
      {
         PropertyDescriptor pd;
         var pdc = new PropertyDescriptorCollection(base.GetProperties(attributes).Cast<PropertyDescriptor>().ToArray());
         if ((pd = pdc.Find("Source", false)) != null)
         {
            pdc.Add(TypeDescriptor.CreateProperty(typeof(Binding), pd, new Attribute[] { new DefaultValueAttribute("null") }));
            pdc.Remove(pd);
         }
         return pdc;
      }
   }

   public class BindingConverter : ExpressionConverter
   {
      public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
      {
         return (destinationType == typeof(MarkupExtension)) ? true : false;
      }
      public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
      {
         if (destinationType == typeof(MarkupExtension))
         {
            var bindingExpression = value as BindingExpression;
            if (bindingExpression == null) throw new Exception();
            return bindingExpression.ParentBinding;
         }

         return base.ConvertTo(context, culture, value, destinationType);
      }
   }

   public static class XamlHelper
   {
      static XamlHelper()
      {
         // this is absolutely vital:
         TypeDescriptor.AddProvider(new BindingTypeDescriptionProvider(), typeof(Binding));
         TypeDescriptor.AddAttributes(typeof(BindingExpression), new Attribute[] { new TypeConverterAttribute(typeof(BindingConverter)) });
      }

      public static string RoundTripXaml(string xaml)
      {
         return XamlWriter.Save(XamlReader.Parse(xaml));
      }

      public static string ConvertToXaml(object wpf)
      {
         return XamlWriter.Save(wpf);
      }
   }
}



"@ -language CSharpVersion3 -reference PresentationCore, PresentationFramework, WindowsBase -OutputType Library -OutputAssembly PoshWpf.dll

Again, you can get an executable by just altering the last line like so:

"@ -language CSharpVersion3 -reference PresentationCore, PresentationFramework, WindowsBase -OutputType ConsoleApplication -OutputAssembly PoshWpf.exe
A: 

I'm no powershell dev, but have you tried escaping the { } with ` ? Perhaps it's trying to be clever and evaluate the binding as a powershell expression?

Steven Robbins
That's a good thought. In my test case, the {} are being inserted in the C# Main() method just for the purpose of avoiding such differences ... and I deliberately used Console.WriteLine so they're not "output" that will be parsed by PowerShell at all.
Jaykul
A: 

I am a little bit confused about the TypeConverter setup you're doing in XamlHelper's type initializer. What is BindingConverter supposed to do? Are you intending for the {Binding} markup extension to be handled the way it normally does in WPF?

In any case, markup extensions cannot round trip through XAML which is by design. The following excerpt from an MSDN page regarding XAML serialization limitations:

Common references to objects made by various markup extension formats, such as StaticResource or Binding, will be dereferenced by the serialization process. These were already dereferenced at the time that in-memory objects were created by the application runtime, and the Save logic does not revisit the original XAML to restore such references to the serialized output. This potentially freezes any databound or resource obtained value to be the value last used by the run-time representation, with only limited or indirect ability to distinguish such a value from any other value set locally. Images are also serialized as object references to images as they exist in the project, rather than as original source references, losing whatever filename or URI was originally referenced. Even resources declared within the same page are seen serialized into the point where they were referenced, rather than being preserved as a key of a resource collection

Given that, I'm not sure why on earth it should even work at all in a compiled application. But as I said, I must admit I am not sure what you're doing with the TypeConverter so maybe you've already addressed the above limitation.

Josh Einstein
The BindingConverter provides the save logic for the binding expression (without caring about the "source" per-se), which for now, is perfect for what I'm doing ... since we want to set the source later anyway.
Jaykul
Incidentally, the BindingCustomTypeDescriptor and BindingTypeDescriptionProvider actually do attempt to serialize the source in case it's a static-reference, but I haven't really got any useful examples of where I would use that (ie: where it would make sense to round-trip that), so I'm not really using it at the moment.
Jaykul
Check these for more details on BindingConverter - http://www.codeproject.com/KB/WPF/xamlwriterandbinding.aspx and http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/a1984451-4840-4e0f-abc5-8a8e34a4f8ca
akjoshi