diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
index 336ad58d11..92dee9430c 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ControlResolverBase.cs
@@ -202,7 +202,7 @@ public IControlResolverMetadata ResolveControl(IControlType controlType)
/// Returns a list of possible DotVVM controls.
/// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives.
- public abstract IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes();
+ public abstract IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes();
///
/// Gets the control metadata.
diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
index 759ce41b2a..59d0507420 100644
--- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs
@@ -265,7 +265,7 @@ private IAbstractControl ProcessObjectElement(DothtmlElementNode element, IDataC
var similarNameHelp = similarControls.Any() ? $" Did you mean {string.Join(", ", similarControls.Select(c => c.tagPrefix + ":" + c.name))}, or other DotVVM control?" : "";
var tagPrefixHelp = configuration.Markup.Controls.Any(c => string.Equals(c.TagPrefix, element.TagPrefix, StringComparison.OrdinalIgnoreCase))
? ""
- : $" {(similarNameHelp is null ? "Make" : "Otherwise, make")} sure that the tagPrefix '{element.TagPrefix}' is registered in DotvvmConfiguration.Markup.Controls collection!";
+ : $" {(similarNameHelp is "" ? "Make" : "Otherwise, make")} sure that the tagPrefix '{element.TagPrefix}' is registered in DotvvmConfiguration.Markup.Controls collection!";
element.TagNameNode.AddError($"The control <{element.FullTagName}> could not be resolved!{similarNameHelp}{tagPrefixHelp}");
}
if (controlMetadata.VirtualPath is {} && controlMetadata.Type.IsAssignableTo(ResolvedTypeDescriptor.Create(typeof(DotvvmView))))
@@ -512,10 +512,10 @@ from c in this.controlResolver.EnumerateControlTypes()
where controlBaseType is null || c.type.Type.IsAssignableTo(controlBaseType)
let prefixScore = tagPrefix is null ? 0 : StringSimilarity.DamerauLevenshteinDistance(c.tagPrefix, tagPrefix)
where prefixScore <= threshold
- from controlName in Enumerable.Concat([ c.type.Type.Name ], c.type.AlternativeNames)
+ from controlName in Enumerable.Concat([ c.tagName ?? c.type.PrimaryName ], c.type.AlternativeNames)
let nameScore = StringSimilarity.DamerauLevenshteinDistance(elementName.ToLowerInvariant(), controlName.ToLowerInvariant())
where prefixScore + nameScore <= threshold
- orderby prefixScore + nameScore descending
+ orderby (prefixScore + nameScore, controlName, c.tagPrefix) descending
select (c.tagPrefix, controlName, c.type)
).Take(limit).ToArray();
}
diff --git a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
index 2274fb4469..7405493110 100644
--- a/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/DefaultControlResolver.cs
@@ -300,7 +300,7 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type)
return new ControlResolverMetadata((ControlType)type);
}
- public override IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes()
+ public override IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes()
{
var markupControls = new HashSet<(string, string)>(); // don't report MarkupControl with @baseType twice
@@ -308,8 +308,15 @@ public override IControlResolverMetadata BuildControlMetadata(IControlType type)
{
if (!string.IsNullOrEmpty(control.Src))
{
- var markupControl = FindMarkupControl(control.Src);
markupControls.Add((control.TagPrefix!, control.TagName!));
+ IControlType? markupControl = null;
+ try
+ {
+ markupControl = FindMarkupControl(control.Src);
+ }
+ catch { } // ignore the error, we should not crash here
+ if (markupControl != null)
+ yield return (control.TagPrefix!, control.TagName, markupControl);
}
}
@@ -327,7 +334,8 @@ type.DeclaringType is null &&
typeof(DotvvmBindableObject).IsAssignableFrom(type) &&
namespaces.TryGetValue(type.Namespace ?? "", out var controlConfig))
{
- yield return (controlConfig.TagPrefix!, new ControlType(type));
+ if (!markupControls.Contains((controlConfig.TagPrefix!, type.Name)))
+ yield return (controlConfig.TagPrefix!, null, new ControlType(type));
}
}
}
diff --git a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
index 130e27b071..fb2d92c6ff 100644
--- a/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
+++ b/src/Framework/Framework/Compilation/ControlTree/IControlResolver.cs
@@ -29,7 +29,7 @@ public interface IControlResolver
/// Returns a list of possible DotVVM controls.
/// Used only for smart error handling, the list isn't necessarily complete, but doesn't contain false positives.
- IEnumerable<(string tagPrefix, IControlType type)> EnumerateControlTypes();
+ IEnumerable<(string tagPrefix, string? tagName, IControlType type)> EnumerateControlTypes();
///
/// Resolves the binding type.
diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs
index 70ba70ea53..dabdab4307 100644
--- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs
+++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTests.cs
@@ -218,6 +218,22 @@ public void ResolvedTree_UnknownPrefix()
Assert.AreEqual("The control could not be resolved! Did you mean dot:Button, or other DotVVM control? Otherwise, make sure that the tagPrefix 'bp' is registered in DotvvmConfiguration.Markup.Controls collection!", XAssert.Single(node.NodeErrors));
}
+
+ [TestMethod]
+ public void ResolvedTree_SimilarToMarkupControl()
+ {
+ var root = ParseSource(@"@viewModel string
+
+
+");
+
+ var controls = root.Content.Where(c => !c.IsOnlyWhitespace()).ToArray();
+ var node = (controls[0].DothtmlNode as DothtmlElementNode).TagNameNode;
+ Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithPropertyDirective, or other DotVVM control?", XAssert.Single(node.NodeErrors));
+ node = (controls[1].DothtmlNode as DothtmlElementNode).TagNameNode;
+ Assert.AreEqual("The control could not be resolved! Did you mean cmc:ControlWithBaseType, or other DotVVM control?", XAssert.Single(node.NodeErrors));
+ }
+
[TestMethod]
public void ResolvedTree_ElementProperty()
{
diff --git a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs
index 29c08ac877..f1998d7fb6 100644
--- a/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs
+++ b/src/Tests/Runtime/ControlTree/DefaultControlTreeResolver/DefaultControlTreeResolverTestsBase.cs
@@ -1,7 +1,11 @@
-using DotVVM.Framework.Compilation.ControlTree.Resolved;
+using DotVVM.Framework.Binding;
+using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Configuration;
+using DotVVM.Framework.Controls;
+using DotVVM.Framework.Hosting;
using DotVVM.Framework.Testing;
using DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolver;
+using Microsoft.Extensions.DependencyInjection;
namespace DotVVM.Framework.Tests.Runtime.ControlTree
{
@@ -11,13 +15,43 @@ public abstract class DefaultControlTreeResolverTestsBase
static DefaultControlTreeResolverTestsBase()
{
- configuration = DotvvmTestHelper.CreateConfiguration();
+ var fakeMarkupFileLoader = new FakeMarkupFileLoader() {
+ MarkupFiles = {
+ ["ControlWithBaseType.dotcontrol"] = """
+ @viewModel object
+ @baseType DotVVM.Framework.Tests.Runtime.ControlTree.DefaultControlTreeResolverTestsBase.TestMarkupControl1
+
+ {{value: Text}}
+ """,
+ ["ControlWithPropertyDirective.dotcontrol"] = """
+ @viewModel object
+ @property string Text
+
+ {{value: Text}}
+ """
+ }
+ };
+ configuration = DotvvmTestHelper.CreateConfiguration(s => {
+ s.AddSingleton(fakeMarkupFileLoader);
+ });
configuration.Markup.AddCodeControls("cc", typeof(ClassWithInnerElementProperty));
+ configuration.Markup.AddMarkupControl("cmc", "ControlWithBaseType", "ControlWithBaseType.dotcontrol");
+ configuration.Markup.AddMarkupControl("cmc", "ControlWithPropertyDirective", "ControlWithPropertyDirective.dotcontrol");
configuration.Freeze();
}
protected ResolvedTreeRoot ParseSource(string markup, string fileName = "default.dothtml", bool checkErrors = false) =>
DotvvmTestHelper.ParseResolvedTree(markup, fileName, configuration, checkErrors);
+ public class TestMarkupControl1: DotvvmMarkupControl
+ {
+ public string Text
+ {
+ get { return (string)GetValue(TextProperty); }
+ set { SetValue(TextProperty, value); }
+ }
+ public static readonly DotvvmProperty TextProperty =
+ DotvvmProperty.Register(nameof(Text));
+ }
}
}