Skip to content

Commit

Permalink
Improve the error message when ExtensionParameters are executed in in…
Browse files Browse the repository at this point in the history
…valid contexts

When the data context stack is shallower than expected,
it might receive null control.
  • Loading branch information
exyi committed Nov 3, 2024
1 parent 24401ae commit 7a2dfae
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataItemContainer>()
.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.");
Expand Down Expand Up @@ -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<T>(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<IStaticCommandServiceLoader>().GetStaticCommandService(type, context);
return (T)context.Services.GetRequiredService<IStaticCommandServiceLoader>().GetStaticCommandService(typeof(T), context);
#pragma warning restore CS0618
}

Expand Down Expand Up @@ -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();
}

Expand Down
15 changes: 13 additions & 2 deletions src/Framework/Framework/Utils/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -100,9 +101,19 @@ public static int FindIndex<T>(this IEnumerable<T> enumerable, Func<T, bool> pre
return -1;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T NotNull<T>([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<K, V> ToSorted<K, V>(this IDictionary<K, V> d, IComparer<K>? c = null)
where K: notnull =>
Expand Down
22 changes: 22 additions & 0 deletions src/Tests/Binding/BindingCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Exception>(() => 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()
{
Expand Down
15 changes: 0 additions & 15 deletions src/Tests/ControlTests/GridViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,21 +154,6 @@ vvv enabled customer vvv
check.CheckString(r.FormattedHtml, fileExtension: "html");
}


[TestMethod]
public async Task SortedChangedStaticCommand()
{
var r = await cth.RunPage(typeof(BasicTestViewModel), """
<dot:GridView DataSource={value: EmptyCustomers} SortChanged={staticCommand: (string dir) => _js.Invoke("resort", dir)}>
<Columns>
<dot:GridViewTextColumn HeaderText=Name ValueBinding={value: Name} AllowSorting />
</Columns>
</dot:GridView>
""", directives: "@js dotvvm.internal");

check.CheckString(r.FormattedHtml, fileExtension: "html");
}

[TestMethod]
public async Task GridViewColumn_FormatString_ResourceBinding()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<body>

<!-- ko if: Customers()?.Items()?.length -->
<table data-bind="dotvvm-gridviewdataset: { dataSet: Customers(), mapping: {} }">
<table data-bind="dotvvm-gridviewdataset: {'mapping':{},'dataSet':Customers()}">
<thead>
<tr>
<th>
Expand Down

0 comments on commit 7a2dfae

Please sign in to comment.