Skip to content

Commit

Permalink
DataContext resource binding: GridView support
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Nov 23, 2024
1 parent c8172a2 commit 23c5f55
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 60 deletions.
2 changes: 1 addition & 1 deletion src/DynamicData/DynamicData/DataContextStackHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ public static DataContextStack CreateChildStack(this DataContextStack dataContex
dataContextStack.BindingPropertyResolvers);
}
}
}
}
8 changes: 5 additions & 3 deletions src/Framework/Framework/Binding/BindingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,12 @@ public static Func<TParam, TResult> Cache<TParam, TResult>(this Func<TParam, TRe
return f => cache.GetOrAdd(f, func);
}

public static IValueBinding GetThisBinding(this DotvvmBindableObject obj)
public static IStaticValueBinding GetThisBinding(this DotvvmBindableObject obj)
{
var dataContext = obj.GetValueBinding(DotvvmBindableObject.DataContextProperty);
return (IValueBinding)dataContext!.GetProperty<ThisBindingProperty>().binding;
var dataContext = (IStaticValueBinding?)obj.GetBinding(DotvvmBindableObject.DataContextProperty);
if (dataContext is null)
throw new InvalidOperationException("DataContext must be set to a binding to allow creation of a {value: _this} binding");
return (IStaticValueBinding)dataContext!.GetProperty<ThisBindingProperty>().binding;
}

private static readonly ConditionalWeakTable<Expression, BindingParameterAnnotation> _expressionAnnotations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public ControlPropertyTypeDataContextChangeAttribute(string propertyName, int or
throw new Exception($"The property '{PropertyName}' was not found on control '{controlType}'!");
}

if (control.properties.Contains(controlProperty) && control.GetValueBinding(controlProperty) is IValueBinding valueBinding)
if (control.properties.Contains(controlProperty) && control.GetBinding(controlProperty) is IStaticValueBinding valueBinding)
{
return valueBinding.ResultType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using DotVVM.Framework.Compilation.ViewCompiler;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Binding.Expressions;
using FastExpressionCompiler;

namespace DotVVM.Framework.Compilation.ControlTree
{
Expand Down Expand Up @@ -407,12 +408,19 @@ private void ProcessAttribute(IPropertyDescriptor property, DothtmlAttributeNode
attribute.ValueNode.AddError($"The property '{ property.FullName }' cannot contain {bindingNode.Name} binding.");
}
var binding = ProcessBinding(bindingNode, dataContext, property);
if (property.IsBindingProperty)
{
// check that binding types are compatible
if (!property.PropertyType.IsAssignableFrom(ResolvedTypeDescriptor.Create(binding.BindingType)))
{
attribute.ValueNode.AddError($"The property '{property.FullName}' cannot contain a binding of type '{binding.BindingType}'!");
}
}
var bindingProperty = treeBuilder.BuildPropertyBinding(property, binding, attribute);
if (!treeBuilder.AddProperty(control, bindingProperty, out var error)) attribute.AddError(error);
}
else
{
// hard-coded value in markup
var textValue = (DothtmlValueTextNode)attribute.ValueNode;

try
Expand Down
65 changes: 36 additions & 29 deletions src/Framework/Framework/Controls/GridView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ private void DataBind(IDotvvmRequestContext context)
head = null;

var dataSourceBinding = GetDataSourceBinding();
var serverOnly = dataSourceBinding is not IValueBinding;
var dataSource = DataSource;

CreateHeaderRow(context);
Expand All @@ -212,7 +213,7 @@ private void DataBind(IDotvvmRequestContext context)
foreach (var item in GetIEnumerableFromDataSource()!)
{
// create row
var placeholder = new DataItemContainer { DataItemIndex = index };
var placeholder = new DataItemContainer { DataItemIndex = index, RenderItemBinding = !serverOnly };
placeholder.SetDataContextTypeFromDataSource(dataSourceBinding);
placeholder.DataContext = item;
placeholder.SetValue(Internal.PathFragmentProperty, GetPathFragmentExpression() + "/[" + index + "]");
Expand Down Expand Up @@ -289,7 +290,7 @@ protected virtual void CreateHeaderRow(IDotvvmRequestContext context)

var dataContextStack = this.GetDataContextType()!;
var commandType = LoadData is { } ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default;
var gridViewBindings = gridViewDataSetBindingProvider.GetGridViewBindings(dataContextStack, (IValueBinding)GetDataSourceBinding(), commandType);
var gridViewBindings = gridViewDataSetBindingProvider.GetGridViewBindings(dataContextStack, GetDataSourceBinding(), commandType);

return (gridViewBindings, SortChanged is { } ? BuildDefaultSortCommandBinding() : null);
}
Expand Down Expand Up @@ -471,19 +472,22 @@ protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext
head?.Render(writer, context);

// render body
var foreachBinding = TryGetKnockoutForeachExpression().NotNull("GridView does not support DataSource={resource: ...} at this moment.");
if (RenderOnServer)
var foreachBinding = TryGetKnockoutForeachExpression();
if (foreachBinding is {})
{
writer.AddKnockoutDataBind("dotvvm-SSR-foreach", "{data:" + foreachBinding + "}");
}
else
{
writer.AddKnockoutForeachDataBind(foreachBinding);
if (RenderOnServer)
{
writer.AddKnockoutDataBind("dotvvm-SSR-foreach", "{data:" + foreachBinding + "}");
}
else
{
writer.AddKnockoutForeachDataBind(foreachBinding);
}
}
writer.RenderBeginTag("tbody");

// render contents
if (RenderOnServer)
if (RenderOnServer || foreachBinding is null)
{
// render on server
var index = 0;
Expand Down Expand Up @@ -519,16 +523,19 @@ protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext
{
if (!ShowHeaderWhenNoData)
{
writer.WriteKnockoutDataBindComment("if",
GetForeachDataBindExpression().GetProperty<DataSourceLengthBinding>().Binding.CastTo<IValueBinding>().GetKnockoutBindingExpression(this));
if (GetForeachDataBindExpression().GetProperty<DataSourceLengthBinding>().Binding is IValueBinding conditionValueBinding)
{
writer.WriteKnockoutDataBindComment("if", conditionValueBinding.GetKnockoutBindingExpression(this));
}
}

base.RenderBeginTag(writer, context);
}

protected override void RenderControl(IHtmlWriter writer, IDotvvmRequestContext context)
{
if (RenderOnServer && numberOfRows == 0 && !ShowHeaderWhenNoData)
var ssr = RenderOnServer || GetForeachDataBindExpression() is not IValueBinding;
if (ssr && numberOfRows == 0 && !ShowHeaderWhenNoData)
{
emptyDataContainer?.Render(writer, context);
}
Expand All @@ -542,7 +549,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c
{
base.RenderEndTag(writer, context);

if (!ShowHeaderWhenNoData)
if (!ShowHeaderWhenNoData && GetForeachDataBindExpression() is IValueBinding)
{
writer.WriteKnockoutDataBindEndComment();
}
Expand All @@ -552,23 +559,23 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c

protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
{
var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType);
var userColumnMappingService = context.Services.GetRequiredService<UserColumnMappingCache>();
var mapping = userColumnMappingService.GetMapping(itemType!);
var mappingJson = JsonSerializer.Serialize(mapping, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
var dataBinding =
(GetDataSourceBinding() as IValueBinding ?? throw new DotvvmControlException(this, "GridView does not support DataSource={resource: ...} at this moment."))
.GetKnockoutBindingExpression(this, unwrapped: true);

var helperBinding = new KnockoutBindingGroup();
helperBinding.Add("dataSet", dataBinding);
helperBinding.Add("mapping", mappingJson);
if (this.LoadData is { } loadData)
if (GetBinding(DataSourceProperty) is IValueBinding dataBinding)
{
var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context")));
helperBinding.Add("loadDataSet", loadDataExpression);
var itemType = ReflectionUtils.GetEnumerableType(GetDataSourceBinding().ResultType);
var userColumnMappingService = context.Services.GetRequiredService<UserColumnMappingCache>();
var mapping = userColumnMappingService.GetMapping(itemType!);
var mappingJson = JsonSerializer.Serialize(mapping, new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping });

var helperBinding = new KnockoutBindingGroup();
helperBinding.Add("dataSet", dataBinding.GetKnockoutBindingExpression(this, unwrapped: true));
helperBinding.Add("mapping", mappingJson);
if (this.LoadData is { } loadData)
{
var loadDataExpression = KnockoutHelper.GenerateClientPostbackLambda("LoadData", loadData, this, new PostbackScriptOptions(elementAccessor: "$element", koContext: CodeParameterAssignment.FromIdentifier("$context")));
helperBinding.Add("loadDataSet", loadDataExpression);
}
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString());
}
writer.AddKnockoutDataBind("dotvvm-gridviewdataset", helperBinding.ToString());

base.AddAttributesToRender(writer, context);
}
Expand Down
12 changes: 6 additions & 6 deletions src/Framework/Framework/Controls/GridViewBindings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ namespace DotVVM.Framework.Controls
/// <summary> Contains pre-created command and value bindings for the <see cref="GridView" /> components. An instance can be obtained from <see cref="GridViewDataSetBindingProvider" /> </summary>
public class GridViewBindings
{
private readonly ConcurrentDictionary<string, IValueBinding<bool>> isSortColumnAscending = new(concurrencyLevel: 1, capacity: 16);
private readonly ConcurrentDictionary<string, IValueBinding<bool>> isSortColumnDescending = new(concurrencyLevel: 1, capacity: 16);
private readonly ConcurrentDictionary<string, IStaticValueBinding<bool>> isSortColumnAscending = new(concurrencyLevel: 1, capacity: 16);
private readonly ConcurrentDictionary<string, IStaticValueBinding<bool>> isSortColumnDescending = new(concurrencyLevel: 1, capacity: 16);

public ICommandBinding? SetSortExpression { get; init; }

internal IValueBinding<Func<string, bool>>? IsColumnSortedAscending { get; init; }
internal IValueBinding<Func<string, bool>>? IsColumnSortedDescending { get; init; }
internal IStaticValueBinding<Func<string, bool>>? IsColumnSortedAscending { get; init; }
internal IStaticValueBinding<Func<string, bool>>? IsColumnSortedDescending { get; init; }

public IValueBinding<bool>? GetIsColumnSortedAscendingBinding(string sortExpression)
public IStaticValueBinding<bool>? GetIsColumnSortedAscendingBinding(string sortExpression)
{
if (IsColumnSortedAscending == null)
{
Expand All @@ -24,7 +24,7 @@ public class GridViewBindings
return isSortColumnAscending.GetOrAdd(sortExpression, _ => IsColumnSortedAscending.Select(a => a(sortExpression)));
}

public IValueBinding<bool>? GetIsColumnSortedDescendingBinding(string sortExpression)
public IStaticValueBinding<bool>? GetIsColumnSortedDescendingBinding(string sortExpression)
{
if (IsColumnSortedDescending == null)
{
Expand Down
35 changes: 23 additions & 12 deletions src/Framework/Framework/Controls/GridViewDataSetBindingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class GridViewDataSetBindingProvider
private readonly BindingCompilationService service;

private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), DataPagerBindings> dataPagerCommands = new();
private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewBindings> gridViewCommands = new();
private readonly ConcurrentDictionary<(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType), GridViewBindings> gridViewCommands = new();

public GridViewDataSetBindingProvider(BindingCompilationService service)
{
Expand All @@ -37,7 +37,7 @@ public DataPagerBindings GetDataPagerBindings(DataContextStack dataContextStack,
}

/// <summary> Returns pre-created GridView bindings for a given data context and data source (result is cached). </summary>
public GridViewBindings GetGridViewBindings(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType)
public GridViewBindings GetGridViewBindings(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType)
{
return gridViewCommands.GetOrAdd((dataContextStack, dataSetBinding, commandType), x => GetGridViewBindingsCore(x.dataContextStack, x.dataSetBinding, x.commandType));
}
Expand Down Expand Up @@ -132,19 +132,20 @@ ParameterExpression CreateParameter(DataContextStack dataContextStack, string na
};
}

private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextStack, IValueBinding dataSetBinding, GridViewDataSetCommandType commandType)
private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextStack, IStaticValueBinding dataSetBinding, GridViewDataSetCommandType commandType)
{
var isServerOnly = dataSetBinding is not IValueBinding;
var dataSetExpr = dataSetBinding.GetProperty<ParsedExpressionBindingProperty>().Expression;
ICommandBinding? GetCommandOrNull<T>(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression)
{
return typeof(T).IsAssignableFrom(dataSetExpr.Type)
? CreateCommandBinding<T>(commandType, dataSetExpr, dataContextStack, methodName, arguments, transformExpression)
: null;
}
IValueBinding<TResult>? GetValueBindingOrNull<T, TResult>(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression)
IStaticValueBinding<TResult>? GetValueBindingOrNull<T, TResult>(DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression)
{
return typeof(T).IsAssignableFrom(dataSetExpr.Type)
? CreateValueBinding<T, TResult>(dataSetExpr, dataContextStack, methodName, arguments, transformExpression)
? CreateValueBinding<T, TResult>(dataSetExpr, dataContextStack, methodName, arguments, transformExpression, isServerOnly)
: null;
}

Expand Down Expand Up @@ -172,7 +173,7 @@ private GridViewBindings GetGridViewBindingsCore(DataContextStack dataContextSta
};
}

private IValueBinding<TResult> CreateValueBinding<TDataSetInterface, TResult>(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression = null)
private IStaticValueBinding<TResult> CreateValueBinding<TDataSetInterface, TResult>(Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression = null, bool isServerOnly = false)
{
// get concrete type from implementation of IXXXableGridViewDataSet<?>
var optionsConcreteType = GetOptionsConcreteType<TDataSetInterface>(dataSet.Type, out var optionsProperty);
Expand All @@ -188,12 +189,22 @@ private IValueBinding<TResult> CreateValueBinding<TDataSetInterface, TResult>(Ex
expression = transformExpression(expression);
}

return new ValueBindingExpression<TResult>(service,
new object[]
{
new ParsedExpressionBindingProperty(expression),
dataContextStack
});
if (isServerOnly)
{
return new ResourceBindingExpression<TResult>(service,
[
new ParsedExpressionBindingProperty(expression),
dataContextStack
]);
}
else
{
return new ValueBindingExpression<TResult>(service,
[
new ParsedExpressionBindingProperty(expression),
dataContextStack
]);
}
}

private ICommandBinding CreateCommandBinding<TDataSetInterface>(GridViewDataSetCommandType commandType, Expression dataSet, DataContextStack dataContextStack, string methodName, Expression[] arguments, Func<Expression, Expression>? transformExpression = null)
Expand Down
Loading

0 comments on commit 23c5f55

Please sign in to comment.