Skip to content

Commit

Permalink
Full DataSource=resource support in GridView and DataPager
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Nov 24, 2024
1 parent 36f7055 commit 3019446
Show file tree
Hide file tree
Showing 26 changed files with 358 additions and 81 deletions.
2 changes: 1 addition & 1 deletion src/Framework/Core/Controls/GenericGridViewDataSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class GenericGridViewDataSet<
/// <summary>
/// Gets or sets the items for the current page.
/// </summary>
public IList<T> Items { get; set; } = new List<T>();
public virtual IList<T> Items { get; set; } = new List<T>();

/// <summary>
/// Gets or sets whether the data should be refreshed. This property is set to true automatically
Expand Down
27 changes: 27 additions & 0 deletions src/Framework/Core/Controls/ServerSideGridViewDataSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Framework.Controls
{
/// <summary>
/// Represents a collection of items with paging and sorting which keeps the Items collection server-side (Bind(Direction.None)) and only transfers the necessary metadata (page index, sort direction).
/// </summary>
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
public class ServerSideGridViewDataSet<T>()
: GenericGridViewDataSet<T, NoFilteringOptions, SortingOptions, PagingOptions, NoRowInsertOptions, NoRowEditOptions>(new(), new(), new(), new(), new())
{

[Bind(Direction.None)]
public override IList<T> Items { get => base.Items; set => base.Items = value; }
// return specialized dataset options
public new GridViewDataSetOptions GetOptions()
{
return new GridViewDataSetOptions {
FilteringOptions = FilteringOptions,
SortingOptions = SortingOptions,
PagingOptions = PagingOptions
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static string GetMessage(IBinding binding, Type[] properties, string? message, E

var introMsg =
isRequiredProperty ?
$"Could not initialize binding as it is missing a required property {properties[0].Name}" :
$"Could not initialize binding '{binding}' as it is missing a required property {properties[0].Name}" :
$"Unable to get property {properties[0].Name}";

var pathMsg = "";
Expand Down
12 changes: 8 additions & 4 deletions src/Framework/Framework/Binding/DotvvmBindingCacheHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public ValueBindingExpression<TResult> CreateValueBinding<TResult>(string code,
new ValueBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -78,7 +79,8 @@ public ResourceBindingExpression<TResult> CreateResourceBinding<TResult>(string
new ResourceBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -100,7 +102,8 @@ public CommandBindingExpression<TResult> CreateCommand<TResult>(string code, Dat
new CommandBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand All @@ -122,7 +125,8 @@ public StaticCommandBindingExpression<TResult> CreateStaticCommand<TResult>(stri
new StaticCommandBindingExpression<TResult>(compilationService, new object?[] {
dataContext,
new OriginalStringBindingProperty(code),
parserOptions
parserOptions,
new ExpectedTypeBindingProperty(typeof(TResult))
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,6 @@ public CollectionElementDataContextBindingProperty GetCollectionElementDataConte
return new CollectionElementDataContextBindingProperty(DataContextStack.CreateCollectionElement(
ReflectionUtils.GetEnumerableType(resultType.Type).NotNull(),
parent: dataContext,
extensionParameters: new CollectionElementDataContextChangeAttribute(order: 0).GetExtensionParameters(new ResolvedTypeDescriptor(dataContext.DataContextType)).ToArray(),
serverSideOnly: binding is not IValueBinding
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ private DataContextStack(Type type,
BindingPropertyResolvers = bindingPropertyResolvers?.ToImmutableArray() ?? ImmutableArray<Delegate>.Empty;
ServerSideOnly = serverSideOnly;

if (ExtensionParameters.Length >= 2)
{
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var p in ExtensionParameters)
{
if (!set.Add(p.Identifier))
throw new Exception($"Extension parameter '{p.Identifier}' is defined multiple times in the data context stack {this}");
}
}

hashCode = ComputeHashCode();
}

Expand Down
40 changes: 25 additions & 15 deletions src/Framework/Framework/Controls/CheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public bool? Checked
}

public static readonly DotvvmProperty CheckedProperty =
DotvvmProperty.Register<bool?, CheckBox>(t => t.Checked, false);
DotvvmProperty.Register<bool?, CheckBox>(t => t.Checked, null);

/// <summary>
/// Gets or sets a collection of values of all checked checkboxes. Use this property in combination with the CheckedValue property.
Expand Down Expand Up @@ -60,12 +60,14 @@ public bool DisableIndeterminate
/// </summary>
protected override void RenderInputTag(IHtmlWriter writer)
{
if (HasValueBinding(CheckedProperty) && !HasValueBinding(CheckedItemsProperty))
var checkedValue = GetValueRaw(CheckedProperty);
var checkedItemsValue = GetValueRaw(CheckedItemsProperty);
if (checkedValue is {} && checkedItemsValue is null)
{
// boolean mode
RenderCheckedProperty(writer);
RenderCheckedProperty(writer, checkedValue);
}
else if (!HasValueBinding(CheckedProperty) && HasValueBinding(CheckedItemsProperty))
else if (checkedValue is null && checkedItemsValue is {})
{
// collection mode
RenderCheckedItemsProperty(writer);
Expand Down Expand Up @@ -121,18 +123,26 @@ protected virtual void RenderCheckedItemsBinding(IHtmlWriter writer)
writer.AddKnockoutDataBind("dotvvm-checkedItems", checkedItemsBinding!.GetKnockoutBindingExpression(this));
}

protected virtual void RenderCheckedProperty(IHtmlWriter writer)
protected virtual void RenderCheckedProperty(IHtmlWriter writer, object? checkedValue)
{
var checkedBinding = GetValueBinding(CheckedProperty);

// dotvvm-CheckState sets elements to indeterminate state when checkedBinding is null,
// knockout's default checked binding does not do that
var bindingHandler = DisableIndeterminate ? "checked" : "dotvvm-CheckState";
writer.AddKnockoutDataBind(bindingHandler, checkedBinding!, this);

// Boolean mode can have prerendered `checked` attribute
if (RenderOnServer && true.Equals(GetValue(CheckedProperty)))
writer.AddAttribute("checked", null);
if (checkedValue is IValueBinding checkedBinding)
{
// dotvvm-CheckState sets elements to indeterminate state when checkedBinding is null,
// knockout's default checked binding does not do that
var bindingHandler = DisableIndeterminate ? "checked" : "dotvvm-CheckState";
writer.AddKnockoutDataBind(bindingHandler, checkedBinding!, this);

// Boolean mode can have prerendered `checked` attribute
if (RenderOnServer && KnockoutHelper.TryEvaluateValueBinding(this, checkedBinding) is true)
writer.AddAttribute("checked", null);
}
else
{
if ((bool?)EvalPropertyValue(CheckedProperty, checkedValue) is true)
{
writer.AddAttribute("checked", null);
}
}
}


Expand Down
10 changes: 5 additions & 5 deletions src/Framework/Framework/Controls/DataPager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ protected virtual void DataBind(Hosting.IDotvvmRequestContext context)
{
Children.Clear();

var dataSetBinding = GetValueBinding(DataSetProperty)!;
var dataSetBinding = GetDataSetBinding();
var dataSetType = dataSetBinding.ResultType;

var commandType = LoadData is {} ? GridViewDataSetCommandType.LoadDataDelegate : GridViewDataSetCommandType.Default;
Expand Down Expand Up @@ -318,8 +318,9 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest

if (this.LoadData is {} loadData)
{
var dataSetBinding = GetDataSetBinding() as IValueBinding ?? throw new DotvvmControlException(this, "The DataSet property must set to a value binding when LoadData command is used!");
var helperBinding = new KnockoutBindingGroup();
helperBinding.Add("dataSet", GetDataSetBinding().GetKnockoutBindingExpression(this, unwrapped: true));
helperBinding.Add("dataSet", dataSetBinding.GetKnockoutBindingExpression(this, unwrapped: true));
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());
Expand Down Expand Up @@ -356,8 +357,7 @@ protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext c
{
}


private IValueBinding GetDataSetBinding()
=> GetValueBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:DataPager control must be set!");
private IStaticValueBinding GetDataSetBinding()
=> GetValueOrResourceBinding(DataSetProperty) ?? throw new DotvvmControlException(this, "The DataSet property of the dot:DataPager control must be set!");
}
}
16 changes: 16 additions & 0 deletions src/Framework/Framework/Controls/DotvvmBindableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ public void SetValueRaw(DotvvmProperty property, object? value)
return binding as IValueBinding;
}

/// <summary>
/// Gets the value binding set to a specified property. Returns null if the property is not a binding, throws if the binding some kind of command.
/// </summary>
public IStaticValueBinding? GetValueOrResourceBinding(DotvvmProperty property, bool inherit = true)
{
var binding = GetBinding(property, inherit);
if (binding is null)
return null;
if (binding is not IStaticValueBinding valueBinding)
{
// throw exception on incompatible binding types
throw new BindingHelper.BindingNotSupportedException(binding) { RelatedControl = this };
}
return valueBinding;
}

/// <summary> Returns a Javascript (knockout) expression representing value or binding of this property. </summary>
public ParametrizedCode GetJavascriptValue(DotvvmProperty property, bool inherit = true) =>
GetValueOrBinding<object>(property, inherit).GetParametrizedJsExpression(this);
Expand Down
12 changes: 3 additions & 9 deletions src/Framework/Framework/Controls/GridView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,9 @@ private static void SetCellAttributes(GridViewColumn column, HtmlGenericControl

if (!isHeaderCell)
{
var cssClassBinding = column.GetValueBinding(GridViewColumn.CssClassProperty);
if (cssClassBinding != null)
{
cellAttributes["class"] = cssClassBinding;
}
else if (!string.IsNullOrWhiteSpace(column.CssClass))
{
cellAttributes["class"] = column.CssClass;
}
if (column.GetValueRaw(GridViewColumn.CssClassProperty) is {} cssClassBinding &&
cssClassBinding is not "")
cellAttributes.Set("class", cssClassBinding);
}
else
{
Expand Down
15 changes: 8 additions & 7 deletions src/Framework/Framework/Controls/GridViewCheckBoxColumn.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DotVVM.Framework.Binding;
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Hosting;

Expand All @@ -14,13 +15,13 @@ public class GridViewCheckBoxColumn : GridViewColumn
/// Gets or sets a binding which retrieves the value to display from the current data item.
/// </summary>
[MarkupOptions(AllowHardCodedValue = false, Required = true)]
public bool ValueBinding
public IStaticValueBinding<bool?> ValueBinding
{
get { return (bool)GetValue(ValueBindingProperty)!; }
get { return (IStaticValueBinding<bool?>)GetValueRaw(ValueBindingProperty)!; }
set { SetValue(ValueBindingProperty, value); }
}
public static readonly DotvvmProperty ValueBindingProperty =
DotvvmProperty.Register<bool, GridViewCheckBoxColumn>(c => c.ValueBinding);
DotvvmProperty.Register<IStaticValueBinding<bool?>, GridViewCheckBoxColumn>(c => c.ValueBinding);

/// <summary> Whether to automatically attach Validator.Value onto the TextBox or add a standalone Validator component. </summary>
public ValidatorPlacement ValidatorPlacement
Expand Down Expand Up @@ -51,17 +52,17 @@ private void CreateControlsCore(DotvvmControl container, bool enabled)
var checkBox = new CheckBox { Enabled = enabled };
CopyProperty(UITests.NameProperty, checkBox, UITests.NameProperty);

var valueBinding = GetValueBinding(ValueBindingProperty);
checkBox.SetBinding(CheckBox.CheckedProperty, valueBinding);
Validator.Place(checkBox, container.Children, valueBinding, ValidatorPlacement);
var binding = ValueBinding;
checkBox.SetBinding(CheckBox.CheckedProperty, binding);
Validator.Place(checkBox, container.Children, binding as IValueBinding, ValidatorPlacement);
container.Children.Add(checkBox);
}

protected override string? GetSortExpression()
{
if (string.IsNullOrEmpty(SortExpression))
{
return GetValueBinding(ValueBindingProperty)?.GetProperty<OriginalStringBindingProperty>()?.Code ??
return GetBinding(ValueBindingProperty)?.GetProperty<OriginalStringBindingProperty>()?.Code ??
throw new DotvvmControlException(this, $"The 'ValueBinding' property must be set on the '{GetType()}' control!");
}
else
Expand Down
Loading

0 comments on commit 3019446

Please sign in to comment.