Skip to content

Commit

Permalink
Merge branch 'main' into release/4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasherceg committed Jan 4, 2023
2 parents 4fa5216 + 7238070 commit 6c3947e
Show file tree
Hide file tree
Showing 55 changed files with 931 additions and 121 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</PropertyGroup>

<PropertyGroup Label="Building">
<LangVersion>10.0</LangVersion>
<LangVersion>11.0</LangVersion>
<!-- Disable warning for missing XML doc comments. -->
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
<Deterministic>true</Deterministic>
Expand Down
19 changes: 8 additions & 11 deletions src/Framework/Framework/Binding/DotvvmCapabilityProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,20 +339,17 @@ internal static IControlAttributeDescriptor InitializeArgument(ICustomAttributeP

var isNullable = propertyType.IsNullable() || type.IsNullable();
if (!defaultValue.HasValue && !isNullable)
dotvvmProperty.MarkupOptions.Required = true;
dotvvmProperty.MarkupOptions._required ??= true;

if (typeof(IBinding).IsAssignableFrom(propertyType))
dotvvmProperty.MarkupOptions.AllowHardCodedValue = false;
dotvvmProperty.MarkupOptions._allowHardCodedValue ??= false;
else if (!typeof(ValueOrBinding).IsAssignableFrom(propertyType.UnwrapNullableType()))
dotvvmProperty.MarkupOptions.AllowBinding = false;

if (!attributeProvider.IsDefined(typeof(MarkupOptionsAttribute), true))
{
if (typeof(IDotvvmObjectLike).IsAssignableFrom(type) ||
typeof(ITemplate).IsAssignableFrom(type) ||
typeof(IEnumerable<IDotvvmObjectLike>).IsAssignableFrom(type))
dotvvmProperty.MarkupOptions.MappingMode = MappingMode.InnerElement;
}
dotvvmProperty.MarkupOptions._allowBinding ??= false;

if (typeof(IDotvvmObjectLike).IsAssignableFrom(type) ||
typeof(ITemplate).IsAssignableFrom(type) ||
typeof(IEnumerable<IDotvvmObjectLike>).IsAssignableFrom(type))
dotvvmProperty.MarkupOptions._mappingMode ??= MappingMode.InnerElement;

DotvvmProperty.Register(dotvvmProperty);
}
Expand Down
13 changes: 10 additions & 3 deletions src/Framework/Framework/Controls/DotvvmControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,12 +429,19 @@ public T FindControlByClientId<T>(string id, bool throwIfNotFound = false) where
DotvvmControl? result = this;
for (var i = 0; i < parts.Length; i++)
{
result = result.GetAllDescendants(c => !IsNamingContainer(c))
.SingleOrDefault(c => c.GetValue(Internal.UniqueIDProperty) as string == parts[i]);
if (result == null)
var results = result.GetAllDescendants(c => !IsNamingContainer(c))
.Where(c => c.GetValue(Internal.UniqueIDProperty) as string == parts[i]).ToArray();
if (results.Length == 0)
{
return null;
}
if (results.Length > 1)
{
throw new DotvvmControlException(results[0], $"Multiple controls with the same UniqueID '{string.Join("_", parts.Take(i + 1))}' were found:" +
string.Concat(results.Take(20).Select(c => "\n * " + c.DebugString(multiline: false))));
}

result = results[0];
}
return result;
}
Expand Down
14 changes: 13 additions & 1 deletion src/Framework/Framework/Controls/HierarchyRepeater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using DotVVM.Framework.Binding.Expressions;
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation.Javascript;
using DotVVM.Framework.Compilation.Javascript.Ast;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.ResourceManagement;
Expand Down Expand Up @@ -244,6 +245,7 @@ private DotvvmControl CreateServerItem(
var itemWrapper = ItemWrapperCapability.GetWrapper();
c.Add(itemWrapper);
var dataItem = new DataItemContainer { DataItemIndex = index };
dataItem.SetValue(Internal.UniqueIDProperty, index.ToString() + "L"); // must be different from sibling HierarchyRepeaterLevel
itemWrapper.Children.Add(dataItem);
dataItem.SetDataContextTypeFromDataSource(GetDataSourceBinding());
// NB: the placeholder is needed because during data context resolution DataItemContainers are looked up
Expand All @@ -256,6 +258,7 @@ private DotvvmControl CreateServerItem(
Internal.PathFragmentProperty,
$"{GetPathFragmentExpression()}{parentSegment}/[{index}]");
placeholder.SetValue(Internal.UniqueIDProperty, "item");
placeholder.SetDataContextTypeFromDataSource(GetDataSourceBinding()); // DataContext type has to be duplicated on the placeholder, because BindingHelper.FindDataContextTarget (in v4.1)
dataItem.Children.Add(placeholder);
ItemTemplate.BuildContent(context, placeholder);

Expand All @@ -280,6 +283,15 @@ p is JavascriptTranslator.ViewModelSymbolicParameter vm ?
return itemWrapper;
}

private static ParametrizedCode indexPathExpression =
JavascriptTranslator.KnockoutContextParameter
.ToExpression()
.Member("$indexPath")
.Member("map").Invoke(new JsIdentifierExpression("ko.unwrap"))
.Member("join").Invoke(new JsLiteral("_"))
.Binary(BinaryOperatorType.Plus, new JsLiteral("L"))
.FormatParametrizedScript();

private DotvvmControl AddClientItemTemplate(IList<DotvvmControl> c, IDotvvmRequestContext context)
{
var bindingService = context.Services.GetRequiredService<BindingCompilationService>();
Expand All @@ -291,7 +303,7 @@ private DotvvmControl AddClientItemTemplate(IList<DotvvmControl> c, IDotvvmReque
var clientIdFragmentProperty = ValueBindingExpression.CreateBinding<string?>(
bindingService.WithoutInitialization(),
h => null,
new ParametrizedCode("$indexPath.map(ko.unwrap).join(\"_\")", OperatorPrecedence.Max),
indexPathExpression,
dataItem.GetDataContextType());
dataItem.SetValue(Internal.ClientIDFragmentProperty, clientIdFragmentProperty);
c.Add(dataItem);
Expand Down
50 changes: 43 additions & 7 deletions src/Framework/Framework/Controls/MarkupOptionsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,23 @@ public class MarkupOptionsAttribute : Attribute
/// <summary>
/// Gets or sets whether client-side data-bindings can be used on this property (`value` and `controlProperty`).
/// </summary>
public bool AllowBinding { get; set; } = true;
public bool AllowBinding
{
get => _allowBinding ?? true;
set => _allowBinding = value;
}

internal bool? _allowBinding = null;

/// <summary>
/// Gets or sets whether the server-side value in markup can be used on this property. Allows both value hard-coded in markup and `resource` binding, which is always evaluated server-side.
/// </summary>
public bool AllowHardCodedValue { get; set; } = true;
public bool AllowHardCodedValue
{
get => _allowHardCodedValue ?? true;
set => _allowHardCodedValue = value;
}
internal bool? _allowHardCodedValue = null;

/// <summary>
/// Gets or sets the name in markup. Null means that the name of the property should be used.
Expand All @@ -30,24 +41,49 @@ public class MarkupOptionsAttribute : Attribute
/// <summary>
/// Determines if multiple property assignments can be merged into one value. For example `&lt;div class='x' class='y' ...` is equivalent to `&lt;div class='x y'` because of the merging.
/// </summary>
public bool AllowValueMerging { get; set; }
public bool AllowValueMerging
{
get => _allowValueMerging ?? false;
set => _allowValueMerging = value;
}
internal bool? _allowValueMerging = null;

/// <summary>
/// Type with non parametric constructor which implements IAttributeValueMerger interface
/// </summary>
public Type AttributeValueMerger { get; set; } = typeof(DefaultAttributeValueMerger);
public Type AttributeValueMerger
{
get => _attributeValueMerger ?? typeof(DefaultAttributeValueMerger);
set => _attributeValueMerger = value;
}
internal Type? _attributeValueMerger = null;

/// <summary>
/// Gets or sets the mapping mode - whether the property is used as an attribute or inner element (or both are allowed).
/// </summary>
public MappingMode MappingMode { get; set; } = MappingMode.Attribute;
public MappingMode MappingMode
{
get => _mappingMode ?? MappingMode.Attribute;
set => _mappingMode = value;
}
internal MappingMode? _mappingMode = null;

/// <summary>
/// Determines whether attributes without value are allowed.
/// </summary>
public bool AllowAttributeWithoutValue { get; set; }
public bool AllowAttributeWithoutValue
{
get => _allowAttributeWithoutValue ?? false;
set => _allowAttributeWithoutValue = value;
}
internal bool? _allowAttributeWithoutValue = null;

/// <summary> Whether the property must always be specified on this control. It is also allowed to set the property using server-side styles. </summary>
public bool Required { get; set; }
public bool Required
{
get => _required ?? false;
set => _required = value;
}
internal bool? _required = null;
}
}
3 changes: 3 additions & 0 deletions src/Framework/Framework/Controls/RouteLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public string RouteName
public static readonly DotvvmProperty RouteNameProperty =
DotvvmProperty.Register<string, RouteLink>(c => c.RouteName);

/// <summary>
/// Gets or sets a value indicating whether the link is enabled and can be clicked on. Please note that the HTML hyperlinks don't support the disabled state, so setting this property to "false" will still produce the "click" event in JavaScript. If the link is disabled, DotVVM will not perform the navigation.
/// </summary>
public bool Enabled
{
get { return (bool)GetValue(EnabledProperty)!; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ protected virtual void WriteException(IErrorWriter w, Exception exc)

w.WriteUnencoded("<div class='exception'><span class='exceptionType'>");
w.WriteText(exc.GetType().FullName);
w.WriteUnencoded("</span><span class='exceptionMessage'>");
w.WriteUnencoded("</span><pre class='exceptionMessage'>");
w.WriteText(exc.Message);
w.WriteUnencoded("</span>");
w.WriteUnencoded("</pre>");
if (source != null)
{
w.WriteSourceCode(source, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public string TransformText()
<button type=button id=save-and-share-button class=execute title='Saves the error as HTML so you can share it with your coworkers'>Save and Share</button>
</div>
<h1>Server Error, HTTP {ErrorCode}: {WebUtility.HtmlEncode(ErrorDescription)}</h1>
<p class=summary>{WebUtility.HtmlEncode(Summary)}</p>
<pre class=summary>{WebUtility.HtmlEncode(Summary)}</pre>
<hr />
<div>
");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ protected virtual void WriteException(IErrorWriter w, ExceptionModel model)
}
w.WriteUnencoded("<div class='exception'><span class='exceptionType'>");
w.WriteText(model.TypeName);
w.WriteUnencoded("</span><span class='exceptionMessage'>");
w.WriteUnencoded("</span><pre class='exceptionMessage'>");
w.WriteText(model.Message);
w.WriteUnencoded("</span><hr />");
w.WriteUnencoded("</pre><hr />");
if (model.AdditionalInfo != null && model.AdditionalInfo.Length > 0)
{
w.WriteUnencoded("<div class='exceptionAdditionalInfo'>");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
for (const [n, v] of Object.entries(props)) {

if (ko.isObservable(v)) {
result[n] = "state" in v ? (v as DotvvmObservable<any>).state : unmapKnockoutObservables(v)
result[n] = "state" in v ? (v as DotvvmObservable<any>).state : unmapKnockoutObservables(v, true)
} else {
result[n] = v
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function createWrapperComputed<T>(accessor: () => KnockoutObservable<T> | T, pro
Object.defineProperty(computed, "state", {
get: () => {
const x = accessor() as any
return (x && x.state) ?? unmapKnockoutObservables(x)
return (x && x.state) ?? unmapKnockoutObservables(x, true)
},
configurable: false,
enumerable: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ type TypeMetadata = ObjectTypeMetadata | EnumTypeMetadata;
type PropertyMetadata = {
type: TypeDefinition;
post?: "always" | "pathOnly" | "no";
update?: "always" | "firstRequest" | "no";
update?: "always" | "no";
validationRules?: PropertyValidationRuleInfo[];
clientExtenders?: ClientExtenderInfo[]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DotvvmPostbackError } from '../shared-classes';
import { getKnownTypes, updateTypeInfo } from '../metadata/typeMap';
import { isPrimitive } from '../utils/objects';
import * as stateManager from '../state-manager'
import { mapUpdatableProperties } from '../serialization/deserialize';

let lastStartedPostbackId: number;

Expand Down Expand Up @@ -135,7 +136,7 @@ async function processPostbackResponse(options: PostbackOptions, context: any, p
let isSuccess = false;
if (result.action == "successfulCommand") {
updateTypeInfo(result.typeMetadata)
result.viewModel = updater.patchViewModel(getState(), result.viewModel)
result.viewModel = updater.patchViewModel(getState(), mapUpdatableProperties(result.viewModel))
updater.updateViewModelAndControls(result);
events.postbackViewModelUpdated.trigger({
...options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,31 @@ export function extendToObservableArrayIfRequired(observable: any) {
function isTypeIdProperty(prop: string) {
return prop == "$type";
}

/** Clones only updatable properties from the object.
* Used after postback, to avoid updating properties which were sent to server, but weren't sent back. */
export function mapUpdatableProperties(viewModel: any, type: TypeDefinition | undefined = undefined): any {
if (isPrimitive(viewModel)) {
return viewModel
}

if (Array.isArray(viewModel)) {
return viewModel.map(item => mapUpdatableProperties(item, (type as TypeDefinition[])?.[0]))
}

let result: any = {}
const typeMetadata = getObjectTypeInfo(viewModel["$type"] ?? type)
for (const prop of keys(viewModel)) {
let value = viewModel[prop]
if (!isTypeIdProperty(prop)) {
const propInfo = typeMetadata?.properties[prop]
if (propInfo?.update == "no") {
continue
}

value = mapUpdatableProperties(value, propInfo?.type)
}
result[prop] = value
}
return result
}
Loading

0 comments on commit 6c3947e

Please sign in to comment.