diff --git a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs index c6ff6b4663..70348305cf 100644 --- a/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs +++ b/src/Framework/Framework/Compilation/ControlTree/BindingExtensionParameter.cs @@ -77,7 +77,8 @@ public class CurrentCollectionIndexExtensionParameter : BindingExtensionParamete } internal static int GetIndex(DotvvmBindableObject c) => - (c.GetAllAncestors(true, false) + (c.NotNull("control is null, is the binding executed in the right data context?") + .GetAllAncestors(true, false) .OfType() .FirstOrDefault() ?? throw new DotvvmControlException(c, "Could not find ancestor DataItemContainer that stores the current collection index.")) .DataItemIndex ?? throw new DotvvmControlException(c, "Nearest DataItemContainer does have the collection index specified."); @@ -141,18 +142,25 @@ public class InjectedServiceExtensionParameter : BindingExtensionParameter public InjectedServiceExtensionParameter(string identifier, ITypeDescriptor type) : base(identifier, type, inherit: true) { } + private static MethodInfo ResolveStaticCommandServiceMethod = typeof(InjectedServiceExtensionParameter).GetMethod(nameof(ResolveStaticCommandService), BindingFlags.NonPublic | BindingFlags.Static)!; + public override Expression GetServerEquivalent(Expression controlParameter) { var type = ResolvedTypeDescriptor.ToSystemType(this.ParameterType); - var expr = ExpressionUtils.Replace((DotvvmBindableObject c) => ResolveStaticCommandService(c, type), controlParameter); - return Expression.Convert(expr, type); + return Expression.Call( + ResolveStaticCommandServiceMethod.MakeGenericMethod(type), + controlParameter + ); } - private object ResolveStaticCommandService(DotvvmBindableObject c, Type type) + private static T ResolveStaticCommandService(DotvvmBindableObject control) { - var context = (IDotvvmRequestContext)c.GetValue(Internal.RequestContextProperty, true).NotNull(); + if (control is null) + throw new ArgumentNullException(nameof(control), "control is null, is the binding executed in the right data context?"); + var context = (IDotvvmRequestContext)control.GetValue(Internal.RequestContextProperty, true) + .NotNull("Current control does not not have the Internal.RequestContextProperty property"); #pragma warning disable CS0618 - return context.Services.GetRequiredService().GetStaticCommandService(type, context); + return (T)context.Services.GetRequiredService().GetStaticCommandService(typeof(T), context); #pragma warning restore CS0618 } @@ -229,7 +237,7 @@ public override Expression GetServerEquivalent(Expression controlParameter) internal static ClaimsPrincipal? GetUser(DotvvmBindableObject control) { - var context = control.GetValue(Internal.RequestContextProperty) as IDotvvmRequestContext; + var context = control.NotNull("control is null, is the binding executed in the right data context?").GetValue(Internal.RequestContextProperty) as IDotvvmRequestContext; return context?.HttpContext?.User ?? new ClaimsPrincipal(); } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObject.cs b/src/Framework/Framework/Controls/DotvvmBindableObject.cs index cfe65eee3e..5df4711773 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObject.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObject.cs @@ -447,5 +447,14 @@ protected internal void CopyProperty(DotvvmProperty sourceProperty, DotvvmBindab throw new DotvvmControlException(this, $"Value of {sourceProperty.FullName} couldn't be copied to targetProperty: {targetProperty.FullName}, because {sourceProperty.FullName} is not set."); } } + + // TODO: make public in next major version + internal void CopyPropertyRaw(DotvvmProperty sourceProperty, DotvvmBindableObject target, DotvvmProperty targetProperty) + { + if (IsPropertySet(sourceProperty)) + { + target.SetValueRaw(targetProperty, GetValueRaw(sourceProperty)); + } + } } } diff --git a/src/Framework/Framework/Controls/GridViewTextColumn.cs b/src/Framework/Framework/Controls/GridViewTextColumn.cs index 476f8e316b..2d1c23c65d 100644 --- a/src/Framework/Framework/Controls/GridViewTextColumn.cs +++ b/src/Framework/Framework/Controls/GridViewTextColumn.cs @@ -77,9 +77,9 @@ public static readonly DotvvmProperty ValidatorPlacementProperty public override void CreateControls(IDotvvmRequestContext context, DotvvmControl container) { var literal = new Literal(); - literal.FormatString = FormatString; - CopyProperty(UITests.NameProperty, literal, UITests.NameProperty); + CopyPropertyRaw(FormatStringProperty, literal, Literal.FormatStringProperty); + CopyPropertyRaw(UITests.NameProperty, literal, UITests.NameProperty); literal.SetBinding(Literal.TextProperty, ValueBinding); Validator.Place(literal, container.Children, ValueBinding, ValidatorPlacement); @@ -95,10 +95,10 @@ public override void CreateEditControls(IDotvvmRequestContext context, DotvvmCon } var textBox = new TextBox(); - textBox.FormatString = FormatString; + CopyPropertyRaw(FormatStringProperty, textBox, TextBox.FormatStringProperty); textBox.SetBinding(TextBox.TextProperty, ValueBinding); - textBox.SetBinding(TextBox.ChangedProperty, ChangedBinding); + CopyPropertyRaw(ChangedBindingProperty, textBox, TextBox.ChangedProperty); Validator.Place(textBox, container.Children, ValueBinding, ValidatorPlacement); container.Children.Add(textBox); } diff --git a/src/Framework/Framework/Utils/FunctionalExtensions.cs b/src/Framework/Framework/Utils/FunctionalExtensions.cs index 7dd383c860..d9cf6f1113 100644 --- a/src/Framework/Framework/Utils/FunctionalExtensions.cs +++ b/src/Framework/Framework/Utils/FunctionalExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -100,9 +101,19 @@ public static int FindIndex(this IEnumerable enumerable, Func pre return -1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T NotNull([NotNull] this T? target, string message = "Unexpected null value.") - where T : class => - target ?? throw new Exception(message); + where T : class + { + if (target is null) + NotNullThrowHelper(message); + return target; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + [DoesNotReturn] + private static void NotNullThrowHelper(string message) => + throw new Exception(message); public static SortedDictionary ToSorted(this IDictionary d, IComparer? c = null) where K: notnull => diff --git a/src/Tests/Binding/BindingCompilationTests.cs b/src/Tests/Binding/BindingCompilationTests.cs index 1c7f2c6c8c..07f9551e48 100755 --- a/src/Tests/Binding/BindingCompilationTests.cs +++ b/src/Tests/Binding/BindingCompilationTests.cs @@ -1207,6 +1207,28 @@ public void Error_DifferentDataContext() ); } + [TestMethod] + public void Error_MissingDataContext_ExtensionParameter() + { + var type = DataContextStack.Create(typeof(string), parent: DataContextStack.Create(typeof(TestViewModel), extensionParameters: [ new InjectedServiceExtensionParameter("config", ResolvedTypeDescriptor.Create(typeof(DotvvmConfiguration)))])); + var control = new PlaceHolder(); + var context = DotvvmTestHelper.CreateContext(); + control.SetDataContextType(type.Parent); + control.DataContext = new TestViewModel(); + control.SetValue(Internal.RequestContextProperty, context); + + var nested = new PlaceHolder(); + control.Children.Add(nested); + + var exception = XAssert.ThrowsAny(() => ExecuteBinding("config.ApplicationPhysicalPath", type, nested)); + XAssert.Contains("data context", exception.Message); + + // check that the error goes away when the data context is set properly + nested.SetDataContextType(type); + nested.DataContext = "test"; + Assert.AreEqual(".", ExecuteBinding("config.ApplicationPhysicalPath", type, nested)); + } + [TestMethod] public void NullableIntAssignment() { diff --git a/src/Tests/ControlTests/GridViewTests.cs b/src/Tests/ControlTests/GridViewTests.cs index 7f2b980ccd..5390fddfa1 100644 --- a/src/Tests/ControlTests/GridViewTests.cs +++ b/src/Tests/ControlTests/GridViewTests.cs @@ -169,11 +169,24 @@ public async Task SortedChangedStaticCommand() check.CheckString(r.FormattedHtml, fileExtension: "html"); } + [TestMethod] + public async Task GridViewColumn_FormatString_ResourceBinding() + { + var r = await cth.RunPage(typeof(BasicTestViewModel), """ + + + + + """); + check.CheckString(r.FormattedHtml, fileExtension: "html"); + } public class BasicTestViewModel: DotvvmViewModelBase { [Bind(Name = "int")] public int Integer { get; set; } = 10000000; + public string FormatString { get; set; } = "00.00"; + public GridViewDataSet Customers { get; set; } = new GridViewDataSet() { RowEditOptions = { EditRowId = 1, diff --git a/src/Tests/ControlTests/testoutputs/GridViewTests.GridViewColumn_FormatString_ResourceBinding.html b/src/Tests/ControlTests/testoutputs/GridViewTests.GridViewColumn_FormatString_ResourceBinding.html new file mode 100644 index 0000000000..be683511cb --- /dev/null +++ b/src/Tests/ControlTests/testoutputs/GridViewTests.GridViewColumn_FormatString_ResourceBinding.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ global format + + local format +
+ + + +
+ 02.00 + + 000002 +
+ + +