Skip to content

Commit

Permalink
feat(xaml): support named params in MarkupExtension
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoy312 committed Mar 19, 2022
1 parent 4fc5c92 commit 428b08c
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NUnit.Framework;

namespace Uno.Xaml.Tests.Test.System.Xaml
{
[TestFixture]
public class ParsedMarkupExtensionInfoTests
{
public static IEnumerable GetSliceParametersTestCases()
{
yield return new TestCaseData("{Binding Path}").Returns(new[] { "Path" });
yield return new TestCaseData("{Binding Path=Path}").Returns(new[] { "Path=Path" });
yield return new TestCaseData("{Binding Path, Converter={StaticResource asd}}").Returns(new[]
{
"Path",
"Converter={StaticResource asd}"
});
yield return new TestCaseData("{Binding Test={some:NestedExtension With,Multiple,Paremeters}, Prop2='Asd'}").Returns(new[]
{
"Test={some:NestedExtension With,Multiple,Paremeters}",
"Prop2='Asd'"
});
yield return new TestCaseData("{Binding Test='text,with,commas,in', Prop1={some:NestedExtension With,Multiple,Paremeters}, Prop2='Asd'}").Returns(new[]
{
"Test='text,with,commas,in'",
"Prop1={some:NestedExtension With,Multiple,Paremeters}",
"Prop2='Asd'"
});
yield return new TestCaseData("{Binding Test1='{}{escaped open bracket', Prop1={some:NestedExtension}, Test2='}close bracket'}").Returns(new[]
{
"Test1='{}{escaped open bracket'",
"Prop1={some:NestedExtension}",
"Test2='}close bracket'"
});
yield return new TestCaseData("{Binding Test1='{', Prop1={some:NestedExtension}, Test2='}close bracket'}").Returns(new[]
{
"Test1='{'",
"Prop1={some:NestedExtension}",
"Test2='}close bracket'"
});
yield return new TestCaseData("{Binding Test1=value without single-quot and with space is legal, Prop2=asd}").Returns(new[]
{
"Test1=value without single-quot and with space is legal",
"Prop2=asd"
});
yield return new TestCaseData("{Binding Test1='{}{however to use the {} escape or comma, you need the single-quots}'}").Returns(new[]
{
"Test1='{}{however to use the {} escape or comma, you need the single-quots}'"
});
}

[Test]
[TestCaseSource(nameof(GetSliceParametersTestCases))]
public string[] SliceParametersTest(string raw)
{
// extract only the vargs portion without the starting `{Binding` and the ending `}`
var vargs = Regex.Match(raw, "^{[^ ]+( (?<vargs>.+))?}$").Groups["vargs"].Value;

return ParsedMarkupExtensionInfo.SliceParameters(vargs, raw).ToArray();
}
}
}
2 changes: 2 additions & 0 deletions src/SourceGenerators/System.Xaml/Assembly/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@
#if !MOBILE && !NETSTANDARD2_0 && !NET5_0
[assembly: TypeForwardedTo (typeof (System.Windows.Markup.ValueSerializerAttribute))]
#endif

[assembly: InternalsVisibleTo("Uno.Xaml.Tests")]
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Expand All @@ -32,28 +32,6 @@ namespace Uno.Xaml
{
internal class ParsedMarkupExtensionInfo
{
/// <summary>
/// This regex returns the members of a binding expression which are separated
/// by commas but keeps the commas inside the member value.
/// e.g. [Property], ConverterParameter='A', TargetNullValue='B', FallbackValue='C,D,E,F' returns
/// - [Property]
/// - ConverterParameter='A'
/// - TargetNullValue='B'
/// - FallbackValue='C,D,E,F'
/// </summary>
private static Regex BindingMembersRegex = new Regex("[^'\",]+'[^^']+'|[^'\",]+\"[^\"]+\"|[^,]+");
private static Regex BalancedMarkupBlockRegex = new Regex(@"
# modified from: https://stackoverflow.com/a/7899205
{ # First '{'
(?:
[^{}]| # Match all non-braces
(?<open>{)| # Match '{', and capture into 'open'
(?<-open>}) # Match '}', and delete the 'open' capture
)+?
(?(open)(?!)) # Fails if 'open' stack isn't empty!
} # Last '}'
", RegexOptions.IgnorePatternWhitespace);

Dictionary<XamlMember, object> args = new Dictionary<XamlMember, object>();
public Dictionary<XamlMember, object> Arguments
{
Expand All @@ -74,55 +52,40 @@ public static ParsedMarkupExtensionInfo Parse(string raw, IXamlNamespaceResolver
throw Error("Invalid markup extension attribute. It should begin with '{{', but was {0}", raw);
}

var ret = new ParsedMarkupExtensionInfo();
int idx = raw.LastIndexOf('}');
if (raw.Length >= 2 && raw[1] == '}')
{
throw Error("Markup extension can not begin with an '{}' escape: '{0}'", raw);
}

if (idx < 0)
var ret = new ParsedMarkupExtensionInfo();
if (raw[raw.Length - 1] != '}')
{
// Any character after the final closing bracket is not accepted. Therefore, the last character should be '}'.
// Ideally, we should still ran the entire markup through the parser to get a more meaningful error.
throw Error("Expected '}}' in the markup extension attribute: '{0}'", raw);
}

raw = raw.Substring(1, idx - 1);
idx = raw.IndexOf(' ');
string name = idx < 0 ? raw : raw.Substring(0, idx);

XamlTypeName xtn;
if (!XamlTypeName.TryParse(name, nsResolver, out xtn))
var nameSeparatorIndex = raw.IndexOf(' ');
var name = nameSeparatorIndex != -1 ? raw.Substring(1, nameSeparatorIndex - 1) : raw.Substring(1, raw.Length - 2);
if (!XamlTypeName.TryParse(name, nsResolver, out var xtn))
{
throw Error("Failed to parse type name '{0}'", name);
}

var xt = sctx.GetXamlType(xtn) ?? new XamlType(xtn.Namespace, xtn.Name, null, sctx);
ret.Type = xt;

if (idx < 0)
if (nameSeparatorIndex < 0)
return ret;

var valueWithoutBinding = raw.Substring(idx + 1, raw.Length - idx - 1);

//var vpairs = BindingMembersRegex.Matches(valueWithoutBinding)
// .Cast<Match>()
// .Select(m => m.Value.Trim())
// .ToList();
//if (vpairs.Count == 0)
//{
// vpairs.Add(valueWithoutBinding);
//}

var innerMarkups = BalancedMarkupBlockRegex.Matches(valueWithoutBinding)
.OfType<Match>(); // needed for net461, netstandard2.0
var indexes = IndexOfAll(valueWithoutBinding, ',')
// ignore those separators used within inner markups
.Where(x => !innerMarkups.Any(y => y.Index <= x && x <= y.Index + y.Length - 1));
var vpairs = SplitByIndex(valueWithoutBinding, indexes)
.Select(x => x.Trim())
.ToList();
var valueWithoutBinding = raw.Substring(nameSeparatorIndex + 1, raw.Length - 1 - (nameSeparatorIndex + 1));
var vpairs = SliceParameters(valueWithoutBinding, raw);

List<string> posPrms = null;
XamlMember lastMember = null;
foreach (var vpair in vpairs)
{
idx = vpair.IndexOf('=');
var idx = vpair.IndexOf('=');

// FIXME: unescape string (e.g. comma)
if (idx < 0)
Expand Down Expand Up @@ -223,21 +186,51 @@ static Exception Error(string format, params object[] args)
return new XamlParseException(String.Format(format, args));
}

static IEnumerable<int> IndexOfAll(string x, char value) => x
.Select(Tuple.Create<char, int>)
.Where(x => x.Item1 == value)
.Select(x => x.Item2);

static IEnumerable<string> SplitByIndex(string x, IEnumerable<int> indexes)
internal static IEnumerable<string> SliceParameters(string vargs, string raw)
{
var previousIndex = 0;
foreach (var index in indexes.OrderBy(i => i))
vargs = vargs.Trim();

// We need to split the parameters by the commas, but with two catches:
// 1. Nested markup extension can also contains multiple parameters, but they are a single parameter to the current context
// 2. Comma can appear within a single-quoted string.
// 3. a little bit of #1 and a little bit #2...
// While we can use regex to match #1 and #2, #3 cannot be solved with regex.

// It seems that single-quot(`'`) can't be escaped when used in the parameters.
// So we don't have to worry about escaping it.

var isInQuot = false;
var bracketDepth = 0;
var lastSliceIndex = -1;

for (int i = 0; i < vargs.Length; i++)
{
var c = vargs[i];
if (false) { }
else if (c == '\'') isInQuot = !isInQuot;
else if (isInQuot) { }
else if (c == '{') bracketDepth++;
else if (c == '}')
{
bracketDepth--;
if (bracketDepth > 0)
{
throw Error("Unexpected '}}' in markup extension: '{0}'", raw);
}
}
else if (c == ',' && bracketDepth == 0)
{
yield return vargs.Substring(lastSliceIndex + 1, i - lastSliceIndex - 1).Trim();
lastSliceIndex = i;
}
}

if (bracketDepth > 0)
{
yield return x.Substring(previousIndex, index - previousIndex);
previousIndex = index + 1;
throw Error("Expected '}}' in markup extension:", raw);
}

yield return x.Substring(previousIndex);
yield return vargs.Substring(lastSliceIndex + 1).Trim();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4868,6 +4868,11 @@ private string BuildBindingOption(XamlMemberDefinition m, INamedTypeSymbol? prop
return BuildLiteralValue(namedTypeSymbol, value.ToString());
}

if (IsCustomMarkupExtensionType(bindingType.Type))
{
return GetCustomMarkupExtensionValue(m);
}

// If type specified in the binding was not found, log and return an error message
if (!string.IsNullOrEmpty(bindingType.Type.Name))
{
Expand Down
16 changes: 16 additions & 0 deletions src/SourceGenerators/XamlGenerationTests/MarkupExtensionTests.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<UserControl x:Class="XamlGenerationTests.MarkupExtensionTests"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="using:XamlGenerationTests.MarkupExtensions">

<UserControl.Resources>
<ext:NotImplementedConverter x:Key="SomeNotImplementedConverter" />
</UserControl.Resources>

<StackPanel>
<TextBlock Text="{Binding Converter={StaticResource SomeNotImplementedConverter}}" />
<!-- TODO: add support for positional parameters -->
<!-- <TextBlock Text="{Binding Converter={StaticResource SomeNotImplementedConverter}, ConverterParameter={ext:MarkupWithArgsExtension 1, 2}}" /> -->
<TextBlock Text="{Binding Converter={StaticResource SomeNotImplementedConverter}, ConverterParameter={ext:MarkupWithArgsExtension Prop1=1, Prop2=2}}" />
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Data;
// using System.Windows.Markup; // <-- will cause MarkupWithArgsExtension to fail
// ^ as it would instead inherits from System.Windows.Markup rather than Windows.UI.Xaml.Markup

namespace XamlGenerationTests
{
public partial class MarkupExtensionTests : UserControl
{
public MarkupExtensionTests()
{
InitializeComponent();
}
}
}

namespace XamlGenerationTests.MarkupExtensions
{
public class NotImplementedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}

public class MarkupWithArgsExtension : MarkupExtension
{
public MarkupWithArgsExtension()
{
}
public MarkupWithArgsExtension(object prop1, object prop2)
{
this.Prop1 = prop1;
this.Prop2 = prop2;
}

[System.Windows.Markup.ConstructorArgument("prop1")]
public object Prop1 { get; set; }

[System.Windows.Markup.ConstructorArgument("prop2")]
public object Prop2 { get; set; }

protected override object ProvideValue() => this;
}
}

0 comments on commit 428b08c

Please sign in to comment.