From c2cf70303e883c7a451b700495eae548a87ea9aa Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Mon, 12 May 2008 08:07:12 +0000 Subject: [PATCH] WPF designer: added markup extension support to XamlDom. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@3070 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Tests/XamlDom/ExampleClass.cs | 13 + .../Tests/XamlDom/SimpleLoadTests.cs | 65 ++++ .../Tests/XamlDom/TestHelper.cs | 14 +- .../Project/MarkupExtensionParser.cs | 320 ++++++++++++++++++ .../Project/WpfDesign.XamlDom.csproj | 7 +- .../WpfDesign.XamlDom/Project/XamlDocument.cs | 7 + .../Project/XamlTextValue.cs | 75 +++- .../DisplayBindings/WpfDesign/WpfDesign.sln | 6 +- 8 files changed, 493 insertions(+), 14 deletions(-) create mode 100644 src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/MarkupExtensionParser.cs diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/ExampleClass.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/ExampleClass.cs index 957b5ebcb1..abad04d998 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/ExampleClass.cs +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/ExampleClass.cs @@ -72,5 +72,18 @@ namespace ICSharpCode.WpfDesign.Tests.XamlDom otherProp2 = value; } } + + object objectProp; + + public object ObjectProp { + get { + TestHelperLog.Log("ObjectProp.get " + Identity); + return objectProp; + } + set { + TestHelperLog.Log("ObjectProp.set to " + value + " - " + Identity); + objectProp = value; + } + } } } diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/SimpleLoadTests.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/SimpleLoadTests.cs index 5a7cea1771..ea9ee5de9c 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/SimpleLoadTests.cs +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/SimpleLoadTests.cs @@ -73,6 +73,19 @@ namespace ICSharpCode.WpfDesign.Tests.XamlDom "); } + [Test] + public void ExampleClassWithEscapedBraceStringPropAttribute() + { + TestLoading(@" + + + "); + } + [Test] public void ExampleClassUseDefaultProperty() { @@ -193,5 +206,57 @@ namespace ICSharpCode.WpfDesign.Tests.XamlDom "); } + + [Test] + public void ExampleClassObjectPropWithStringValue() + { + TestLoading(@" + + + "); + } + + [Test] + public void ExampleClassObjectPropWithTypeValue() + { + TestLoading(@" + + + "); + } + + [Test] + public void ExampleClassObjectPropWithTypeValue2() + { + TestLoading(@" + + + "); + } + + [Test] + public void ExampleClassObjectPropWithNullValue() + { + TestLoading(@" + + + "); + } } } diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/TestHelper.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/TestHelper.cs index 50a0934dc1..a106ea45dd 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/TestHelper.cs +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.Designer/Tests/XamlDom/TestHelper.cs @@ -27,6 +27,13 @@ namespace ICSharpCode.WpfDesign.Tests.XamlDom public static void TestLoading(string xaml) { + Debug.WriteLine("Load using builtin XamlReader:"); + ExampleClass.nextUniqueIndex = 0; + TestHelperLog.logBuilder = new StringBuilder(); + object officialResult = XamlReader.Load(new XmlTextReader(new StringReader(xaml))); + string officialLog = TestHelperLog.logBuilder.ToString(); + Assert.IsNotNull(officialResult, "officialResult is null"); + Debug.WriteLine("Load using own XamlParser:"); ExampleClass.nextUniqueIndex = 0; TestHelperLog.logBuilder = new StringBuilder(); @@ -36,13 +43,6 @@ namespace ICSharpCode.WpfDesign.Tests.XamlDom string ownLog = TestHelperLog.logBuilder.ToString(); Assert.IsNotNull(ownResult, "ownResult is null"); - Debug.WriteLine("Load using builtin XamlReader:"); - ExampleClass.nextUniqueIndex = 0; - TestHelperLog.logBuilder = new StringBuilder(); - object officialResult = XamlReader.Load(new XmlTextReader(new StringReader(xaml))); - string officialLog = TestHelperLog.logBuilder.ToString(); - Assert.IsNotNull(officialResult, "officialResult is null"); - TestHelperLog.logBuilder = null; // compare: string officialSaved = XamlWriter.Save(officialResult); diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/MarkupExtensionParser.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/MarkupExtensionParser.cs new file mode 100644 index 0000000000..58d93af3c2 --- /dev/null +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/MarkupExtensionParser.cs @@ -0,0 +1,320 @@ +// +// +// +// +// $Revision$ +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Windows.Markup; +using System.Xml; + +namespace ICSharpCode.WpfDesign.XamlDom +{ + /// + /// Tokenizer for markup extension attributes. + /// + sealed class MarkupExtensionTokenizer + { + private MarkupExtensionTokenizer() {} + + string text; + int pos; + List tokens = new List(); + + public static List Tokenize(string text) + { + MarkupExtensionTokenizer t = new MarkupExtensionTokenizer(); + t.text = text; + t.Parse(); + return t.tokens; + } + + void AddToken(MarkupExtensionTokenKind kind, string val) + { + tokens.Add(new MarkupExtensionToken(kind, val)); + } + + void Parse() + { + AddToken(MarkupExtensionTokenKind.OpenBrace, "{"); + Expect('{'); + ConsumeWhitespace(); + CheckNotEOF(); + + StringBuilder b = new StringBuilder(); + while (pos < text.Length && !char.IsWhiteSpace(text, pos) && text[pos] != '}') + b.Append(text[pos++]); + AddToken(MarkupExtensionTokenKind.TypeName, b.ToString()); + + ConsumeWhitespace(); + while (pos < text.Length) { + switch (text[pos]) { + case '}': + AddToken(MarkupExtensionTokenKind.CloseBrace, "{"); + pos++; + break; + case '=': + AddToken(MarkupExtensionTokenKind.Equals, "="); + pos++; + break; + case ',': + AddToken(MarkupExtensionTokenKind.Comma, ","); + pos++; + break; + case '{': + throw new XamlMarkupExtensionParseException("'{' is invalid at this location"); + default: + MembernameOrString(); + break; + } + ConsumeWhitespace(); + } + } + + void MembernameOrString() + { + StringBuilder b = new StringBuilder(); + if (text[pos] == '"' || text[pos] == '\'') { + char quote = text[pos++]; + CheckNotEOF(); + while (!(text[pos] == quote && text[pos-1] != '\\')) { + char c = text[pos++]; + if (c != '\\') + b.Append(c); + CheckNotEOF(); + } + pos++; // consume closing quote + ConsumeWhitespace(); + } else { + int braceTotal = 0; + while (braceTotal >= 0) { + CheckNotEOF(); + switch (text[pos]) { + case '\\': + pos++; + CheckNotEOF(); + b.Append(text[pos++]); + break; + case '{': + b.Append(text[pos++]); + braceTotal++; + break; + case '}': + if (braceTotal != 0) { + b.Append(text[pos++]); + } + braceTotal--; + break; + case ',': + case '=': + braceTotal = -1; + break; + default: + b.Append(text[pos++]); + break; + } + } + } + CheckNotEOF(); + string valueText = b.ToString(); + if (text[pos] == '=') { + AddToken(MarkupExtensionTokenKind.Membername, valueText.Trim()); + } else { + AddToken(MarkupExtensionTokenKind.String, valueText); + } + } + + void Expect(char c) + { + CheckNotEOF(); + if (text[pos] != c) + throw new XamlMarkupExtensionParseException("Expected '" + c + "'"); + pos++; + } + + void ConsumeWhitespace() + { + while (pos < text.Length && char.IsWhiteSpace(text, pos)) + pos++; + } + + void CheckNotEOF() + { + if (pos >= text.Length) + throw new XamlMarkupExtensionParseException("Unexpected end of markup extension"); + } + } + + /// + /// Exception thrown when XAML loading fails because there is a syntax error in a markup extension. + /// + [Serializable] + public class XamlMarkupExtensionParseException : XamlLoadException + { + /// + /// Create a new XamlMarkupExtensionParseException instance. + /// + public XamlMarkupExtensionParseException() + { + } + + /// + /// Create a new XamlMarkupExtensionParseException instance. + /// + public XamlMarkupExtensionParseException(string message) : base(message) + { + } + + /// + /// Create a new XamlMarkupExtensionParseException instance. + /// + public XamlMarkupExtensionParseException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Create a new XamlMarkupExtensionParseException instance. + /// + protected XamlMarkupExtensionParseException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + enum MarkupExtensionTokenKind + { + OpenBrace, + CloseBrace, + Equals, + Comma, + TypeName, + Membername, + String + } + + sealed class MarkupExtensionToken + { + public readonly MarkupExtensionTokenKind Kind; + public readonly string Value; + + public MarkupExtensionToken(MarkupExtensionTokenKind kind, string value) + { + this.Kind = kind; + this.Value = value; + } + + public override string ToString() + { + return "[" + Kind + " " + Value + "]"; + } + } + + static class MarkupExtensionParser + { + public static MarkupExtension ConstructMarkupExtension(string attributeText, XmlElement containingElement, XamlDocument document) + { + if (containingElement == null) + throw new ArgumentNullException("containingElement"); + + List markupExtensionTokens = MarkupExtensionTokenizer.Tokenize(attributeText); + if (markupExtensionTokens.Count < 3 + || markupExtensionTokens[0].Kind != MarkupExtensionTokenKind.OpenBrace + || markupExtensionTokens[1].Kind != MarkupExtensionTokenKind.TypeName + || markupExtensionTokens[markupExtensionTokens.Count-1].Kind != MarkupExtensionTokenKind.CloseBrace) + { + throw new XamlMarkupExtensionParseException("Invalid markup extension"); + } + + string typeName = markupExtensionTokens[1].Value; + string typeNamespaceUri; + string typeLocalName; + if (typeName.Contains(":")) { + typeNamespaceUri = containingElement.GetNamespaceOfPrefix(typeName.Substring(0, typeName.IndexOf(':'))); + typeLocalName = typeName.Substring(typeName.IndexOf(':') + 1); + } else { + typeNamespaceUri = containingElement.NamespaceURI; + typeLocalName = typeName; + } + if (string.IsNullOrEmpty(typeNamespaceUri)) + throw new XamlMarkupExtensionParseException("Unrecognized namespace prefix in type " + typeName); + Type extensionType = null; + if (typeNamespaceUri == "http://schemas.microsoft.com/winfx/2006/xaml" && typeLocalName == "Type") { + extensionType = typeof(TypeExtension); + } else { + extensionType = document.TypeFinder.GetType(typeNamespaceUri, typeLocalName + "Extension"); + } + if (extensionType == null || !typeof(MarkupExtension).IsAssignableFrom(extensionType)) { + throw new XamlMarkupExtensionParseException("Unknown markup extension " + typeLocalName + "Extension in " + typeNamespaceUri); + } + + List positionalArgs = new List(); + List> namedArgs = new List>(); + for (int i = 2; i < markupExtensionTokens.Count - 1; i++) { + if (markupExtensionTokens[i].Kind == MarkupExtensionTokenKind.String) { + positionalArgs.Add(markupExtensionTokens[i].Value); + } else if (markupExtensionTokens[i].Kind == MarkupExtensionTokenKind.Membername) { + if (markupExtensionTokens[i+1].Kind != MarkupExtensionTokenKind.Equals + || markupExtensionTokens[i+2].Kind != MarkupExtensionTokenKind.String) + { + throw new XamlMarkupExtensionParseException("Invalid markup extension"); + } + namedArgs.Add(new KeyValuePair(markupExtensionTokens[i].Value, + markupExtensionTokens[i+2].Value)); + i += 2; + } + } + // Find the constructor with positionalArgs.Count arguments + var ctors = extensionType.GetConstructors().Where(c => c.GetParameters().Length == positionalArgs.Count).ToList(); + if (ctors.Count < 1) + throw new XamlMarkupExtensionParseException("No constructor for " + extensionType.FullName + " found that takes " + positionalArgs.Count + " arguments"); + if (ctors.Count > 1) + throw new XamlMarkupExtensionParseException("Multiple constructors for " + extensionType.FullName + " found that take " + positionalArgs.Count + " arguments"); + + var ctorParameters = ctors[0].GetParameters(); + object[] ctorArguments = new object[positionalArgs.Count]; + for (int i = 0; i < ctorArguments.Length; i++) { + TypeConverter c = TypeDescriptor.GetConverter(ctorParameters[i].ParameterType); + ctorArguments[i] = XamlTextValue.AttributeTextToObject(positionalArgs[i], + containingElement, + document, + c); + } + MarkupExtension result = (MarkupExtension)ctors[0].Invoke(ctorArguments); + foreach (var pair in namedArgs) { + string memberName = pair.Key; + if (memberName.Contains(".")) { + throw new NotImplementedException(); + } else { + if (memberName.Contains(":")) + memberName = memberName.Substring(memberName.IndexOf(':') + 1); + var property = extensionType.GetProperty(memberName); + if (property == null) + throw new XamlMarkupExtensionParseException("Property not found: " + extensionType.FullName + "." + memberName); + TypeConverter c = TypeDescriptor.GetConverter(property.PropertyType); + object propValue = XamlTextValue.AttributeTextToObject(pair.Value, + containingElement, + document, + c); + property.SetValue(result, propValue, null); + } + } + return result; + } + + sealed class TypeExtension : System.Windows.Markup.TypeExtension + { + public TypeExtension() {} + + public TypeExtension(string typeName) : base(typeName) + { + } + } + } +} diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/WpfDesign.XamlDom.csproj b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/WpfDesign.XamlDom.csproj index ac5525bb96..fd1f55425c 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/WpfDesign.XamlDom.csproj +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/WpfDesign.XamlDom.csproj @@ -1,4 +1,4 @@ - + {88DA149F-21B2-48AB-82C4-28FB6BDFD783} Debug @@ -18,6 +18,7 @@ -Microsoft.Globalization#CA1303 ..\..\..\..\..\..\AddIns\AddIns\DisplayBindings\WpfDesign\ ..\..\..\..\..\..\AddIns\AddIns\DisplayBindings\WpfDesign\ICSharpCode.WpfDesign.XamlDom.xml + v3.5 true @@ -48,6 +49,9 @@ False + + 3.5 + False @@ -60,6 +64,7 @@ + diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlDocument.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlDocument.cs index 2fd9569a5b..1cfdc4fc4a 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlDocument.cs +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlDocument.cs @@ -26,6 +26,13 @@ namespace ICSharpCode.WpfDesign.XamlDom get { return _xmlDoc; } } + /// + /// Gets the type finder used for this XAML document. + /// + public XamlTypeFinder TypeFinder { + get { return _typeFinder; } + } + /// /// Gets the service provider used for markup extensions in this document. /// diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlTextValue.cs b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlTextValue.cs index f308a08fe7..42309c49fe 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlTextValue.cs +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.XamlDom/Project/XamlTextValue.cs @@ -11,6 +11,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Text; +using System.Windows.Markup; using System.Xml; namespace ICSharpCode.WpfDesign.XamlDom @@ -58,9 +59,12 @@ namespace ICSharpCode.WpfDesign.XamlDom /// public string Text { get { - if (attribute != null) - return attribute.Value; - else if (textValue != null) + if (attribute != null) { + if (attribute.Value.StartsWith("{}")) + return attribute.Value.Substring(2); + else + return attribute.Value; + } else if (textValue != null) return textValue; else if (cDataSection != null) return cDataSection.Value; @@ -105,8 +109,14 @@ namespace ICSharpCode.WpfDesign.XamlDom return b.ToString(); } + static readonly TypeConverter stringTypeConverter = TypeDescriptor.GetConverter(typeof(string)); + internal override object GetValueFor(XamlPropertyInfo targetProperty) { + if (attribute != null) { + return AttributeTextToObject(attribute.Value, attribute.OwnerElement, document, + targetProperty != null ? targetProperty.TypeConverter : stringTypeConverter); + } if (targetProperty == null) return this.Text; TypeConverter converter = targetProperty.TypeConverter; @@ -117,6 +127,65 @@ namespace ICSharpCode.WpfDesign.XamlDom } } + internal static object AttributeTextToObject(string attributeText, XmlElement containingElement, + XamlDocument document, TypeConverter typeConverter) + { + if (typeConverter == null) + typeConverter = stringTypeConverter; + if (attributeText.StartsWith("{}")) { + return typeConverter.ConvertFromString( + document.GetTypeDescriptorContext(), CultureInfo.InvariantCulture, + attributeText.Substring(2)); + } else if (attributeText.StartsWith("{")) { + XamlTypeResolverProvider xtrp = new XamlTypeResolverProvider(document, containingElement, document.ServiceProvider); + MarkupExtension extension = MarkupExtensionParser.ConstructMarkupExtension( + attributeText, containingElement, document); + return extension.ProvideValue(xtrp); + } else { + return typeConverter.ConvertFromString( + document.GetTypeDescriptorContext(), CultureInfo.InvariantCulture, + attributeText); + } + } + + sealed class XamlTypeResolverProvider : IXamlTypeResolver, IServiceProvider + { + XamlDocument document; + XmlElement containingElement; + IServiceProvider baseProvider; + + public XamlTypeResolverProvider(XamlDocument document, XmlElement containingElement, IServiceProvider baseProvider) + { + this.document = document; + this.containingElement = containingElement; + this.baseProvider = baseProvider; + } + + public Type Resolve(string typeName) + { + string typeNamespaceUri; + string typeLocalName; + if (typeName.Contains(":")) { + typeNamespaceUri = containingElement.GetNamespaceOfPrefix(typeName.Substring(0, typeName.IndexOf(':'))); + typeLocalName = typeName.Substring(typeName.IndexOf(':') + 1); + } else { + typeNamespaceUri = containingElement.NamespaceURI; + typeLocalName = typeName; + } + if (string.IsNullOrEmpty(typeNamespaceUri)) + throw new XamlMarkupExtensionParseException("Unrecognized namespace prefix in type " + typeName); + return document.TypeFinder.GetType(typeNamespaceUri, typeLocalName); + } + + public object GetService(Type serviceType) + { + if (serviceType == typeof(IXamlTypeResolver)) + return this; + else + return baseProvider.GetService(serviceType); + } + } + internal override void RemoveNodeFromParent() { if (attribute != null) diff --git a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.sln b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.sln index c16e9ba41c..16d35a4333 100644 --- a/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.sln +++ b/src/AddIns/DisplayBindings/WpfDesign/WpfDesign.sln @@ -1,7 +1,7 @@  -Microsoft Visual Studio Solution File, Format Version 9.00 -# Visual Studio 2005 -# SharpDevelop 2.1.0.2382 +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +# SharpDevelop 3.0.0.3067 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfDesign.XamlDom", "WpfDesign.XamlDom\Project\WpfDesign.XamlDom.csproj", "{88DA149F-21B2-48AB-82C4-28FB6BDFD783}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfDesign.Designer", "WpfDesign.Designer\Project\WpfDesign.Designer.csproj", "{78CC29AC-CC79-4355-B1F2-97936DF198AC}"