Skip to content

Commit ddd5fa4

Browse files
committed
Fix leaking of dictionary changes between cloned controls
1 parent b7983a0 commit ddd5fa4

File tree

3 files changed

+88
-7
lines changed

3 files changed

+88
-7
lines changed

src/Framework/Framework/Controls/DotvvmControlProperties.cs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ private uint hashSeed
4545
}
4646
private bool ownsKeys
4747
{
48-
readonly get => (flags >> 30) != 0;
48+
readonly get => ((flags >> 30) & 1) != 0;
4949
set => flags = (flags & ~(1u << 30)) | ((uint)BoolToInt(value) << 30);
5050
}
5151
private bool ownsValues
5252
{
53-
readonly get => (flags >> 31) != 0;
53+
readonly get => ((flags >> 31) & 1) != 0;
5454
set => flags = (flags & ~(1u << 31)) | ((uint)BoolToInt(value) << 31);
5555
}
5656

@@ -260,6 +260,7 @@ public void Set(DotvvmPropertyId p, object? value)
260260
else
261261
{
262262
Debug.Assert(values is Dictionary<DotvvmPropertyId, object?>);
263+
OwnValues();
263264
valuesAsDictionary[p] = value;
264265
}
265266
}
@@ -320,10 +321,12 @@ public bool TryAdd(DotvvmPropertyId p, object? value)
320321
return Object.ReferenceEquals(existingValue, value);
321322
else
322323
{
324+
OwnValues();
323325
valuesAsDictionary.Add(p, value);
324326
return true;
325327
}
326328
#else
329+
OwnValues();
327330
return valuesAsDictionary.TryAdd(p, value) || Object.ReferenceEquals(valuesAsDictionary[p], value);
328331
#endif
329332
}
@@ -408,23 +411,40 @@ internal void CloneInto(ref DotvvmControlProperties newDict)
408411
else if (this.keys == null)
409412
{
410413
var dictionary = this.valuesAsDictionary;
411-
if (dictionary.Count > 30)
414+
if (dictionary.Count > 8)
412415
{
413416
newDict = this;
414417
newDict.keys = null;
415-
newDict.valuesAsDictionary = new Dictionary<DotvvmPropertyId, object?>(dictionary);
418+
Dictionary<DotvvmPropertyId, object?>? newValues = null;
416419
foreach (var (key, value) in dictionary)
417420
if (CloneValue(value) is {} newValue)
421+
{
422+
if (newValues is null)
423+
// ok, we have to copy it
424+
newValues = new Dictionary<DotvvmPropertyId, object?>(dictionary);
418425
newDict.valuesAsDictionary[key] = newValue;
426+
}
427+
428+
if (newValues is null)
429+
{
430+
newDict.valuesAsDictionary = dictionary;
431+
newDict.ownsValues = false;
432+
this.ownsValues = false;
433+
}
434+
else
435+
{
436+
newDict.valuesAsDictionary = newValues;
437+
newDict.ownsValues = true;
438+
}
419439
return;
420440
}
421-
// move to immutable version if it's reasonably small. It will be probably cloned multiple times again
441+
// move to immutable version if it's small. It will be probably cloned multiple times again
422442
SwitchToPerfectHashing();
423443
}
424444

425445
newDict = this;
426446
newDict.ownsKeys = false;
427-
newDict.ownsValues = false;
447+
this.ownsKeys = false;
428448
for (int i = 0; i < newDict.valuesAsArray.Length; i++)
429449
{
430450
if (CloneValue(newDict.valuesAsArray[i]) is {} newValue)
@@ -439,6 +459,12 @@ internal void CloneInto(ref DotvvmControlProperties newDict)
439459
newDict.valuesAsArray[i] = newValue;
440460
}
441461
}
462+
463+
if (newDict.values == this.values)
464+
{
465+
this.ownsValues = false;
466+
newDict.ownsValues = false;
467+
}
442468
}
443469

444470
[MethodImpl(MethodImplOptions.AggressiveInlining)]

src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi
120120
var requiredResourceControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(RequiredResource)));
121121
o.TreeVisitors.Add(() => new StyleTreeShufflingVisitor(controlResolver));
122122
o.TreeVisitors.Add(() => new ControlPrecompilationVisitor(s));
123-
// o.TreeVisitors.Add(() => new LiteralOptimizationVisitor());
123+
o.TreeVisitors.Add(() => new LiteralOptimizationVisitor());
124124
o.TreeVisitors.Add(() => new BindingRequiredResourceVisitor((ControlResolverMetadata)requiredResourceControl));
125125
var requiredGlobalizeControl = controlResolver.ResolveControl(new ResolvedTypeDescriptor(typeof(GlobalizeResource)));
126126
o.TreeVisitors.Add(() => new GlobalizeResourceVisitor((ControlResolverMetadata)requiredGlobalizeControl));

src/Tests/Runtime/DotvvmPropertyTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ public void DotvvmProperty_CorrectGetAndSet()
303303
foreach (var example in GetExampleValues(p.PropertyType))
304304
{
305305
instance.SetValue(p, example);
306+
Assert.AreEqual(example, instance.GetValue(p), $"GetValue is behaving weird {p}");
307+
Assert.AreEqual(example, instance.GetValueRaw(p.Id), $"GetValue(id) is behaving weird {p}");
306308
Assert.AreEqual(example, p.PropertyInfo.GetValue(instance), $"Getter is broken in {p}");
307309
}
308310

@@ -371,5 +373,58 @@ public void DotvvmProperty_ManyItemsSetter()
371373

372374
Assert.AreEqual(1000, control1.Properties.Count);
373375
}
376+
377+
[TestMethod]
378+
[DataRow(false, false)]
379+
[DataRow(false, true)]
380+
[DataRow(true, false)]
381+
[DataRow(true, true)]
382+
public void DotvvmProperty_ControlClone(bool manyAttributes, bool nestedControl)
383+
{
384+
var control = new HtmlGenericControl("div");
385+
control.CssStyles.Set("color", "red");
386+
control.Attributes.Set("something", "value");
387+
388+
if (manyAttributes)
389+
for (int i = 0; i < 60; i++)
390+
control.Attributes.Set("data-" + i.ToString(), i.ToString());
391+
392+
if (nestedControl)
393+
{
394+
control.Properties.Add(Styles.ReplaceWithProperty, new HtmlGenericControl("span") { InnerText = "23" });
395+
}
396+
397+
var clone = (HtmlGenericControl)control.CloneControl();
398+
399+
Assert.AreEqual(control.TagName, clone.TagName);
400+
Assert.AreEqual(control.CssStyles["color"], "red");
401+
402+
// change original
403+
Assert.IsFalse(clone.CssStyles.ContainsKey("abc"));
404+
control.CssStyles.Set("color", "blue");
405+
control.CssStyles.Set("abc", "1");
406+
Assert.AreEqual("red", clone.CssStyles["color"]);
407+
Assert.IsFalse(clone.CssStyles.ContainsKey("abc"));
408+
409+
if (nestedControl)
410+
{
411+
var nestedClone = (HtmlGenericControl)clone.Properties[Styles.ReplaceWithProperty]!;
412+
var nestedOriginal = (HtmlGenericControl)control.Properties[Styles.ReplaceWithProperty]!;
413+
Assert.AreEqual("23", nestedClone.InnerText);
414+
// change clone this time
415+
nestedClone.InnerText = "24";
416+
Assert.AreEqual("23", nestedOriginal.InnerText);
417+
Assert.AreEqual("24", nestedClone.InnerText);
418+
}
419+
420+
if (manyAttributes)
421+
{
422+
for (int i = 0; i < 60; i++)
423+
{
424+
Assert.AreEqual(i.ToString(), control.Attributes["data-" + i.ToString()]);
425+
Assert.AreEqual(i.ToString(), clone.Attributes["data-" + i.ToString()]);
426+
}
427+
}
428+
}
374429
}
375430
}

0 commit comments

Comments
 (0)