diff --git a/README.md b/README.md
index f7ca5ce..fe33b3a 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,109 @@
-# cloudscribe.Web.Navigation
+This is a fork from [cloudscribe.Web.Navigation](https://github.com/cloudscribe/cloudscribe.Web.Navigation).
-cloudscribe.Web.Navigation ([NuGet](http://www.nuget.org/packages/cloudscribe.Web.Navigation)) provides an easy to use solution for menus, breadcrumbs, and other navigation in ASP.NET Core web applications. It was implemented for use with other cloudscribe components, but it does not depend on other cloudscribe components, and you can use it in your projects even if you are not using other cloudscribe components.
+I've made the following changes to smooth the upgrading work from ASP.net 4.x to ASP.net Core for my projects using [MvcSiteMapProvider](https://github.com/maartenba/MvcSiteMapProvider/).
-For installation instruction and full details, see the documentation: https://www.cloudscribe.com/docs/cloudscribe-web-navigation
+# Background
-The NavigationDemo.Web project has examples such as nodes filtered by roles, and a way to adjust breadcrumbs from a controller action. The demo app also has menu localization in 7 languages with a language switcher to illustrate how you can localize your own menus.
+We were using [MvcSiteMapProvider](https://github.com/maartenba/MvcSiteMapProvider/) for navigation(menus/breadcrumbs) in our .net 4.x project heavily. When planning to move to .net Core, we found it was missing .net Core support yet.
-We use cloudscribe.Web.Navigation for the menus in [cloudscribe Core](https://github.com/joeaudette/cloudscribe), and in [cloudscribe SimpleContent](https://github.com/joeaudette/cloudscribe.SimpleContent)
+From [this discussion](https://github.com/maartenba/MvcSiteMapProvider/issues/394), we learnt the cloudscribe.Web.Navigation project.
-If you have questions please visit our community forums https://www.cloudscribe.com/forum
+In our project, we used the [configuration-by-code feature](https://github.com/maartenba/MvcSiteMapProvider/wiki/Defining-sitemap-nodes-using-.NET-attributes) a lot. However cloudscribe.Web.Navigation did not support this feature. So this is the first reason for we made this fork.
-Follow me on twitter @cloudscribeweb and @joeaudette
+# 1. [NavNodeAttribute]
-[](https://twitter.com/cloudscribeweb) [](https://twitter.com/cloudscribeweb)
+This attribute is just like the [MvcSiteMapNodeAttribute], which makes it convenient to keep the node information in the same place as your controller action.
-### Build Status
+ [NavNode(Key = "ViewPage", ParentKey = "ProductIndexPage", Text = "View page")]
+ public IActionResult View()
+ {
+ return View();
+ }
-| Windows | Linux |
-| ------------- | ------------- |
-| [](https://ci.appveyor.com/project/joeaudette/cloudscribe-web-navigation) | [](https://travis-ci.org/cloudscribe/cloudscribe.Web.Navigation) |
+Comparing with navigation.xml/json,
+* the field information of `controller/action/area` of the node will be collected by reflection automatically;
+* other field information can be set via this attribute by code;
+* `Key` is optional if it has no children node (see #2);
+* `ParentKey` points to its parent node and will be used to build the navigation tree. If empty, it refers to the RootNode;
+* Don't set duplicated keys and the RootNode should be only one.
+* You may set `Order` to adjust the order of the nodes (see #3).
+
+# 2. Key auto-generated
+
+The name of keys are insignificant for most nodes. So we change it optional and generate a random key automatically if you don't set one.
+
+This is not for [NavNode] only. It also works for the navigation.xml or navigation.json.
+
+# 3. Node order
+
+We add `Order` property to NavigationNode. You may adjust the display order of the nodes.
+
+Considering compatibilty, this feature is NOT enabled by default. (see #4)
+
+# 4. Configuration
+
+In appsettings.json, you may configure like below:
+
+ {
+ "NavigationOptions": {
+ "RootTreeBuilderName": "cloudscribe.Web.Navigation.ReflectionNavigationTreeBuilder",
+ "IncludeAssembliesForScan": "NavigationDemo.Web",
+ "EnableSorting": true
+ }
+ }
+
+You should put the assembly names into `IncludeAssembliesForScan` (comma-separated if two or more), and you should set `EnableSorting` to true.
+
+The RootNode of the navigation tree should be marked like below:
+
+ [NavNode(Key = "HomePage", ParentKey = "", Text = "Home page")]
+ public IActionResult Index()
+ {
+ return View();
+ }
+
+# 5. Mixing configuration
+
+You can also use both navigation.xml and [NavNode] (or both navigation.json and [NavNode]). For example in appsettings.json,
+
+ {
+ "NavigationOptions": {
+ "RootTreeBuilderName": "cloudscribe.Web.Navigation.XmlNavigationTreeBuilder",
+ "NavigationMapXmlFileName": "navigation.xml",
+ "IncludeAssembliesForScan": "NavigationDemo.Web",
+ "EnableSorting": true
+ }
+ }
+
+It will load the navigation.xml configuration first, and then load the [NavNode] configuration. Of course, in this situation, the RootNode should be set in the navigation.xml.
+
+# 6. Processing '$resources:....'
+
+MvcSiteMapProvider supports an old localization feature, [which inherited from ASP.net Site Navigation](https://docs.microsoft.com/en-us/previous-versions/aspnet/ms178427(v=vs.100)?redirectedfrom=MSDN). You may use text in format: `$resources:ClassName,KeyName,DefaultValue` for `Title` or `Text` of the node.
+
+# 7. Converting from 'Mvc.sitemap'
+
+We also made a small console tool for converting from 'Mvc.sitemap' to navigation.xml.
+
+# 8. KeyPrefix
+
+We use some inherited controllers, and made this prefix feature. For example,
+
+ public class ProductController : Controller
+ {
+ [NavNode(Key = "{Prefix}ProductList", Text = "List of Products")]
+ public IActionResult List() {}
+ [NavNode(ParentKey = "{Prefix}ProductList", Text = "Product details", PreservedRouteParameters = "id")]
+ public IActionResult View(int id) {}
+ }
+ [Area("Staff")]
+ [NavNodeController(KeyPrefix = "Staff")]
+ public class StaffProductController : ProductController
+ {
+ }
+
+* For `ProductController`, `List` node will has key: `ProductList` (prefix is empty now); `View` node will has parentKey `ProductList`, pointing to `List` action of the same controller;
+* `StaffProductController` inherits from `ProductController`. `List` node will has key `StaffProductList` (prefix is `Staff` now); `View` node will has parentKey `StaffProductList`, pointing to `List` action of the same controller.
+
+
+I'll pull this work to [cloudscribe.Web.Navigation](https://github.com/cloudscribe/cloudscribe.Web.Navigation) later. If accepted and merged, this fork will stop maintainance.
diff --git a/cloudscribe.Web.Navigation.sln b/cloudscribe.Web.Navigation.sln
index ce8198c..12def79 100644
--- a/cloudscribe.Web.Navigation.sln
+++ b/cloudscribe.Web.Navigation.sln
@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cloudscribe.Web.Navigation.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorPages.WebApp", "src\RazorPages.WebApp\RazorPages.WebApp.csproj", "{AFC481E4-5AA5-4998-9C58-49AD1F48C980}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcSitemapConvert", "src\MvcSitemapConvert\MvcSitemapConvert.csproj", "{FD1EB2F9-0C14-4721-B138-9CBEF5CF4E50}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/src/MvcSitemapConvert/MvcSitemapConvert.csproj b/src/MvcSitemapConvert/MvcSitemapConvert.csproj
new file mode 100644
index 0000000..d453e9a
--- /dev/null
+++ b/src/MvcSitemapConvert/MvcSitemapConvert.csproj
@@ -0,0 +1,8 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
diff --git a/src/MvcSitemapConvert/Program.cs b/src/MvcSitemapConvert/Program.cs
new file mode 100644
index 0000000..91b05de
--- /dev/null
+++ b/src/MvcSitemapConvert/Program.cs
@@ -0,0 +1,128 @@
+using System;
+using System.IO;
+using System.Xml;
+
+namespace MvcSitemapConvert
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ var input = "Mvc.sitemap";
+ var output = "navigation.xml";
+ #region preparation
+ if (args != null && args.Length > 0)
+ {
+ input = args[0];
+ if (args.Length > 1)
+ {
+ output = args[1];
+ }
+ }
+ if (!File.Exists(input))
+ {
+ Console.WriteLine("File {0} not exist.", input);
+ return;
+ }
+ if (File.Exists(output))
+ {
+ try
+ {
+ File.Delete(output);
+ }
+ catch
+ {
+ Console.WriteLine("Fail to delete file {0}.", output);
+ return;
+ }
+ }
+ #endregion
+
+ var inputXml = new XmlDocument();
+ inputXml.Load(input);
+
+ var outputXml = new XmlDocument();
+ Convert(inputXml, outputXml);
+
+ using (var fs = new FileStream(output, FileMode.Create))
+ {
+ var writer = XmlWriter.Create(fs, new XmlWriterSettings { Indent = true });
+ outputXml.Save(writer);
+ fs.Close();
+ }
+ Console.WriteLine("Finished.");
+ }
+
+ static void Convert(XmlDocument inputXml, XmlDocument outputXml)
+ {
+ var inputRoot = inputXml.DocumentElement.FirstChild;
+ while (inputRoot.NodeType != XmlNodeType.Element)
+ {
+ inputRoot = inputRoot.NextSibling;
+ }
+ var outputRoot = ConvertNode(inputRoot, outputXml, null);
+ outputXml.AppendChild(outputRoot);
+ }
+
+ static XmlNode ConvertNode(XmlNode inputNode, XmlDocument outputXml, XmlNode parentNode)
+ {
+ var outputNode = outputXml.CreateNode(XmlNodeType.Element, "NavNode", null);
+
+ CopyAttribute(inputNode, "key", outputXml, outputNode, "key");
+ CopyAttribute(inputNode, "title", outputXml, outputNode, "title");
+ CopyAttribute(inputNode, "area", outputXml, outputNode, "area", parentNode);
+ CopyAttribute(inputNode, "controller", outputXml, outputNode, "controller", parentNode);
+ CopyAttribute(inputNode, "action", outputXml, outputNode, "action");
+ CopyAttribute(inputNode, "preservedRouteParameters", outputXml, outputNode, "preservedRouteParameters");
+ CopyAttribute(inputNode, "url", outputXml, outputNode, "url");
+ CopyAttribute(inputNode, "route", outputXml, outputNode, "namedRoute");
+ CopyAttribute(inputNode, "clickable", outputXml, outputNode, "clickable");
+ CopyAttribute(inputNode, "description", outputXml, outputNode, "menuDescription");
+ CopyAttribute(inputNode, "url", outputXml, outputNode, "url");
+ CopyAttribute(inputNode, "order", outputXml, outputNode, "order");
+ CopyAttribute(inputNode, "roles", outputXml, outputNode, "viewRoles");
+ CopyAttribute(inputNode, "targetFrame", outputXml, outputNode, "target");
+ CopyAttribute(inputNode, "visibility", outputXml, outputNode, "componentVisibility");
+
+ var outputChildren = outputXml.CreateNode(XmlNodeType.Element, "Children", null);
+ if (inputNode.ChildNodes.Count > 0)
+ {
+ foreach(XmlNode inputChild in inputNode.ChildNodes)
+ {
+ if (inputChild.NodeType == XmlNodeType.Element)
+ {
+ var outputChild = ConvertNode(inputChild, outputXml, outputNode);
+ outputChildren.AppendChild(outputChild);
+ }
+ else if (inputChild.NodeType == XmlNodeType.Comment)
+ {
+ var outputComment = outputXml.CreateComment(inputChild.InnerText);
+ outputChildren.AppendChild(outputComment);
+ }
+ }
+ }
+ outputNode.AppendChild(outputChildren);
+
+ return outputNode;
+ }
+
+ static void CopyAttribute(XmlNode inputNode, string inputAttrName,
+ XmlDocument outputXml, XmlNode outputNode, string outputAttrName, XmlNode parentNode = null)
+ {
+ var inputAttr = inputNode.Attributes[inputAttrName];
+ if (inputAttr != null)
+ {
+ var outputAttr = outputXml.CreateAttribute(outputAttrName);
+ outputAttr.Value = inputAttr.Value;
+ outputNode.Attributes.Append(outputAttr);
+ }
+ else if (parentNode != null && parentNode.Attributes[outputAttrName] != null)
+ {
+ var outputAttr = outputXml.CreateAttribute(outputAttrName);
+ outputAttr.Value = parentNode.Attributes[outputAttrName].Value;
+ outputNode.Attributes.Append(outputAttr);
+ }
+ }
+
+ }
+}
diff --git a/src/NavigationDemo.Web/Areas/Area51/Controllers/ReflectionController.cs b/src/NavigationDemo.Web/Areas/Area51/Controllers/ReflectionController.cs
new file mode 100644
index 0000000..f7117a5
--- /dev/null
+++ b/src/NavigationDemo.Web/Areas/Area51/Controllers/ReflectionController.cs
@@ -0,0 +1,15 @@
+using cloudscribe.Web.Navigation;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace NavigationDemo.Web.Areas.Area51.Controllers
+{
+ [Area("Area51")]
+ [NavNodeController(KeyPrefix = "Area51")]
+ public class ReflectionController : NavigationDemo.Web.Controllers.ReflectionController
+ {
+ }
+}
diff --git a/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Index.cshtml b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Index.cshtml
new file mode 100644
index 0000000..ab837fe
--- /dev/null
+++ b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Index.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Reflection";
+}
+
+
Reflection Index (in Area 51)
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mulan.cshtml b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mulan.cshtml
new file mode 100644
index 0000000..a04df2d
--- /dev/null
+++ b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mulan.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Mulan";
+}
+
+Mulan (in Area 51)
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mushu.cshtml b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mushu.cshtml
new file mode 100644
index 0000000..95759f8
--- /dev/null
+++ b/src/NavigationDemo.Web/Areas/Area51/Views/Reflection/Mushu.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Mushu";
+}
+
+Mushu (in Area 51)
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Controllers/HomeController.cs b/src/NavigationDemo.Web/Controllers/HomeController.cs
index 3cd15bf..7030b68 100644
--- a/src/NavigationDemo.Web/Controllers/HomeController.cs
+++ b/src/NavigationDemo.Web/Controllers/HomeController.cs
@@ -6,11 +6,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Http;
-
+using cloudscribe.Web.Navigation;
+
namespace NavigationDemo.Web.Controllers
{
public class HomeController : Controller
{
+ [NavNode(Key = "Home", ParentKey = "", Text = "Home")]
public IActionResult Index()
{
return View();
diff --git a/src/NavigationDemo.Web/Controllers/ReflectionController.cs b/src/NavigationDemo.Web/Controllers/ReflectionController.cs
new file mode 100644
index 0000000..8035163
--- /dev/null
+++ b/src/NavigationDemo.Web/Controllers/ReflectionController.cs
@@ -0,0 +1,35 @@
+using cloudscribe.Web.Navigation;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace NavigationDemo.Web.Controllers
+{
+ public class ReflectionController : Controller
+ {
+ [NavNode(Key = "{Prefix}ReflectionIndex", ParentKey = "{Prefix}Home", Order = -9,
+ Text = "Reflection", ResourceType = typeof(Resources.MyResource))]
+ public virtual IActionResult Index()
+ {
+ return View();
+ }
+
+ [NavNode(Key = "{Prefix}ReflectionMulan", ParentKey = "{Prefix}ReflectionIndex",
+ Text = "Mulan", ResourceType = typeof(Resources.MyResource))]
+ public IActionResult Mulan()
+ {
+ return View();
+ }
+
+ [NavNode(Key = "{Prefix}ReflectionMushu", ParentKey = "{Prefix}ReflectionMulan",
+ Text = "Mushu", ResourceType = typeof(Resources.MyResource))]
+ public IActionResult Mushu()
+ {
+ return View();
+ }
+
+
+ }
+}
diff --git a/src/NavigationDemo.Web/NavigationDemo.Web.csproj b/src/NavigationDemo.Web/NavigationDemo.Web.csproj
index 9e993d8..c361c0b 100644
--- a/src/NavigationDemo.Web/NavigationDemo.Web.csproj
+++ b/src/NavigationDemo.Web/NavigationDemo.Web.csproj
@@ -28,6 +28,21 @@
+
+
+ True
+ True
+ MyResource.resx
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ MyResource.Designer.cs
+
+
+
diff --git a/src/NavigationDemo.Web/Resources/MenuResources.zh-Hans.resx b/src/NavigationDemo.Web/Resources/MenuResources.zh-Hans.resx
index a1fadcf..ff00957 100644
--- a/src/NavigationDemo.Web/Resources/MenuResources.zh-Hans.resx
+++ b/src/NavigationDemo.Web/Resources/MenuResources.zh-Hans.resx
@@ -1,159 +1,159 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- 关于
-
-
- 关于公司
-
-
- 关于我
-
-
- 关于项目
-
-
- 行政
-
-
- 外星人
-
-
- 51区
-
-
- 联系
-
-
- 隐藏匿名
-
-
- 隐藏从认证
-
-
- 家
-
-
- 会员
-
-
- 黑衣人
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 关于
+
+
+ 关于公司
+
+
+ 关于我
+
+
+ 关于项目
+
+
+ 管理
+
+
+ 外星人
+
+
+ 51区
+
+
+ 联系
+
+
+ 匿名不可见
+
+
+ 登录后隐藏
+
+
+ 首页
+
+
+ 会员区
+
+
+ 黑衣人
+
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Resources/MyResource.Designer.cs b/src/NavigationDemo.Web/Resources/MyResource.Designer.cs
new file mode 100644
index 0000000..17cb587
--- /dev/null
+++ b/src/NavigationDemo.Web/Resources/MyResource.Designer.cs
@@ -0,0 +1,117 @@
+//------------------------------------------------------------------------------
+//
+// 此代码由工具生成。
+// 运行时版本:4.0.30319.42000
+//
+// 对此文件的更改可能会导致不正确的行为,并且如果
+// 重新生成代码,这些更改将会丢失。
+//
+//------------------------------------------------------------------------------
+
+namespace NavigationDemo.Web.Resources {
+ using System;
+
+
+ ///
+ /// 一个强类型的资源类,用于查找本地化的字符串等。
+ ///
+ // 此类是由 StronglyTypedResourceBuilder
+ // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+ // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+ // (以 /str 作为命令选项),或重新生成 VS 项目。
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class MyResource {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal MyResource() {
+ }
+
+ ///
+ /// 返回此类使用的缓存的 ResourceManager 实例。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NavigationDemo.Web.Resources.MyResource", typeof(MyResource).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 重写当前线程的 CurrentUICulture 属性,对
+ /// 使用此强类型资源类的所有资源查找执行重写。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// 查找类似 About 的本地化字符串。
+ ///
+ public static string About {
+ get {
+ return ResourceManager.GetString("About", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Aoubt Company 的本地化字符串。
+ ///
+ public static string AboutCompany {
+ get {
+ return ResourceManager.GetString("AboutCompany", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 About Me 的本地化字符串。
+ ///
+ public static string AboutMe {
+ get {
+ return ResourceManager.GetString("AboutMe", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Mulan 的本地化字符串。
+ ///
+ public static string Mulan {
+ get {
+ return ResourceManager.GetString("Mulan", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Mushu 的本地化字符串。
+ ///
+ public static string Mushu {
+ get {
+ return ResourceManager.GetString("Mushu", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Reflection 的本地化字符串。
+ ///
+ public static string Reflection {
+ get {
+ return ResourceManager.GetString("Reflection", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/NavigationDemo.Web/Resources/MyResource.resx b/src/NavigationDemo.Web/Resources/MyResource.resx
new file mode 100644
index 0000000..fecb993
--- /dev/null
+++ b/src/NavigationDemo.Web/Resources/MyResource.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ About
+
+
+ Aoubt Company
+
+
+ About Me
+
+
+ Mulan
+
+
+ Mushu
+
+
+ Reflection
+
+
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Resources/MyResource.zh-Hans.resx b/src/NavigationDemo.Web/Resources/MyResource.zh-Hans.resx
new file mode 100644
index 0000000..62bad33
--- /dev/null
+++ b/src/NavigationDemo.Web/Resources/MyResource.zh-Hans.resx
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 关于
+
+
+ 公司介绍
+
+
+ 关于我
+
+
+ 花木兰
+
+
+ 木须龙
+
+
+ 反射
+
+
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Views/Reflection/Index.cshtml b/src/NavigationDemo.Web/Views/Reflection/Index.cshtml
new file mode 100644
index 0000000..21ce49a
--- /dev/null
+++ b/src/NavigationDemo.Web/Views/Reflection/Index.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Reflection";
+}
+
+Reflection Index
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Views/Reflection/Mulan.cshtml b/src/NavigationDemo.Web/Views/Reflection/Mulan.cshtml
new file mode 100644
index 0000000..76848ff
--- /dev/null
+++ b/src/NavigationDemo.Web/Views/Reflection/Mulan.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Mulan";
+}
+
+Mulan
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/Views/Reflection/Mushu.cshtml b/src/NavigationDemo.Web/Views/Reflection/Mushu.cshtml
new file mode 100644
index 0000000..c73c008
--- /dev/null
+++ b/src/NavigationDemo.Web/Views/Reflection/Mushu.cshtml
@@ -0,0 +1,5 @@
+@{
+ ViewData["Title"] = "Mushu";
+}
+
+Mushu
\ No newline at end of file
diff --git a/src/NavigationDemo.Web/appsettings.json b/src/NavigationDemo.Web/appsettings.json
index 0fa5e21..bc70750 100644
--- a/src/NavigationDemo.Web/appsettings.json
+++ b/src/NavigationDemo.Web/appsettings.json
@@ -6,10 +6,12 @@
"TryGlobalFirst": "true"
},
- "xNavigationOptions": {
- "RootTreeBuilderName": "cloudscribe.Web.Navigation.JsonNavigationTreeBuilder",
+ "NavigationOptions": {
+ "RootTreeBuilderName": "cloudscribe.Web.Navigation.XmlNavigationTreeBuilder",
"NavigationMapXmlFileName": "navigation.xml",
- "NavigationMapJsonFileName": "navigation.json"
+ "NavigationMapJsonFileName": "navigation.json",
+ "IncludeAssembliesForScan": "NavigationDemo.Web",
+ "EnableSorting": true
},
diff --git a/src/NavigationDemo.Web/navigation.xml b/src/NavigationDemo.Web/navigation.xml
index e0e31ad..b65c14a 100644
--- a/src/NavigationDemo.Web/navigation.xml
+++ b/src/NavigationDemo.Web/navigation.xml
@@ -1,12 +1,12 @@
-
+
-
+
-
+
@@ -31,7 +31,7 @@
-
+
diff --git a/src/cloudscribe.Web.Navigation/CloudscribeNavigation.xsd b/src/cloudscribe.Web.Navigation/CloudscribeNavigation.xsd
index 24b9f60..87b461d 100644
--- a/src/cloudscribe.Web.Navigation/CloudscribeNavigation.xsd
+++ b/src/cloudscribe.Web.Navigation/CloudscribeNavigation.xsd
@@ -14,7 +14,7 @@
-
+
diff --git a/src/cloudscribe.Web.Navigation/Constants.cs b/src/cloudscribe.Web.Navigation/Constants.cs
index 3aa01b9..e982947 100644
--- a/src/cloudscribe.Web.Navigation/Constants.cs
+++ b/src/cloudscribe.Web.Navigation/Constants.cs
@@ -9,5 +9,11 @@ namespace cloudscribe.Web.Navigation
public static class Constants
{
public static readonly string TailCrumbsContexctKey = "navigationtailcrumbs";
+
+ public static readonly string XmlNavigationTreeBuilderName = "cloudscribe.Web.Navigation.XmlNavigationTreeBuilder";
+
+ public static readonly string JsonNavigationTreeBuilderName = "cloudscribe.Web.Navigation.JsonNavigationTreeBuilder";
+
+ public static readonly string ReflectionNavigationTreeBuilderName = "cloudscribe.Web.Navigation.ReflectionNavigationTreeBuilder";
}
}
diff --git a/src/cloudscribe.Web.Navigation/NavigationNode.cs b/src/cloudscribe.Web.Navigation/NavigationNode.cs
index a479a75..98080e6 100644
--- a/src/cloudscribe.Web.Navigation/NavigationNode.cs
+++ b/src/cloudscribe.Web.Navigation/NavigationNode.cs
@@ -110,5 +110,10 @@ public NavigationNode()
#endregion
+ ///
+ /// The order number. If you use this, you should set NavigationOptions.EnableSorting to true.
+ ///
+ public int Order { get; set; }
+
}
}
diff --git a/src/cloudscribe.Web.Navigation/NavigationOptions.cs b/src/cloudscribe.Web.Navigation/NavigationOptions.cs
index 7c611a5..017b66d 100644
--- a/src/cloudscribe.Web.Navigation/NavigationOptions.cs
+++ b/src/cloudscribe.Web.Navigation/NavigationOptions.cs
@@ -17,9 +17,22 @@ public class NavigationOptions
public NavigationOptions()
{ }
- public string RootTreeBuilderName { get; set; } = "cloudscribe.Web.Navigation.XmlNavigationTreeBuilder";
+ public string RootTreeBuilderName { get; set; } = Constants.XmlNavigationTreeBuilderName;
public string NavigationMapJsonFileName { get; set; } = "navigation.json";
public string NavigationMapXmlFileName { get; set; } = "navigation.xml";
+
+ ///
+ /// Name of assemblies to scan NavNodeAttributes by reflection.
+ /// Leave it empty to disable this Configuration-by-Code feature.
+ ///
+ public string IncludeAssembliesForScan { get; set; }
+
+ ///
+ /// set to true to enable sorting.
+ /// If you use reflection, you'd better set this to true.
+ /// for compatible reason, default is false.
+ ///
+ public bool EnableSorting { get; set; } = false;
}
}
diff --git a/src/cloudscribe.Web.Navigation/NavigationTreeBuilderService.cs b/src/cloudscribe.Web.Navigation/NavigationTreeBuilderService.cs
index 2cb54fa..d7c2c88 100644
--- a/src/cloudscribe.Web.Navigation/NavigationTreeBuilderService.cs
+++ b/src/cloudscribe.Web.Navigation/NavigationTreeBuilderService.cs
@@ -57,6 +57,16 @@ public async Task> GetTree()
var tree = await _treeCache.GetTree(cacheKey).ConfigureAwait(false);
if(tree != null) { return tree; }
tree = await builder.BuildTree(this).ConfigureAwait(false);
+ if (_navOptions.RootTreeBuilderName != Constants.ReflectionNavigationTreeBuilderName &&
+ !string.IsNullOrEmpty(_navOptions.IncludeAssembliesForScan))
+ {
+ var helper = new NavigationTreeReflectionConverter();
+ await helper.ScanAndMerge(this, _navOptions.IncludeAssembliesForScan, tree).ConfigureAwait(false);
+ }
+ if (_navOptions.EnableSorting)
+ {
+ SortTreeNode(tree);
+ }
await _treeCache.AddToCache(tree, cacheKey);
return tree;
@@ -87,6 +97,17 @@ public async Task ClearTreeCache()
}
+ private void SortTreeNode(TreeNode treeNode)
+ {
+ if (treeNode.Children.Count > 0)
+ {
+ treeNode.Sort();
+ foreach (var child in treeNode.Children)
+ {
+ SortTreeNode(child);
+ }
+ }
+ }
}
}
diff --git a/src/cloudscribe.Web.Navigation/Reflection/NavNodeAttribute.cs b/src/cloudscribe.Web.Navigation/Reflection/NavNodeAttribute.cs
new file mode 100644
index 0000000..40d516f
--- /dev/null
+++ b/src/cloudscribe.Web.Navigation/Reflection/NavNodeAttribute.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace cloudscribe.Web.Navigation
+{
+ ///
+ /// SiteMap node attribute, used to decorate action methods with SiteMap node metadata
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class NavNodeAttribute : Attribute
+ {
+ /// required field for most nodes. Only the RootNode can be without a parent.
+ public string ParentKey { get; set; } = string.Empty;
+ public string Key { get; set; } = string.Empty;
+ public string Text { get; set; } = string.Empty;
+ public string Title { get; set; } = string.Empty;
+ public string Url { get; set; } = string.Empty;
+ public string Page { get; set; } = string.Empty;
+ public string Area { get; set; } = string.Empty;
+ public string NamedRoute { get; set; } = string.Empty;
+ public bool ExcludeFromSearchSiteMap { get; set; } = false;
+ public bool HideFromAuthenticated { get; set; } = false;
+ public bool HideFromAnonymous { get; set; } = false;
+ public string PreservedRouteParameters { get; set; } = string.Empty;
+ public string ComponentVisibility { get; set; } = string.Empty;
+ public string AuthorizationPolicy { get; set; } = string.Empty;
+ public string ViewRoles { get; set; } = string.Empty;
+ public string CustomData { get; set; } = string.Empty;
+ public bool IsClickable { get; set; } = true;
+ public string IconCssClass { get; set; } = string.Empty;
+ public string CssClass { get; set; } = string.Empty;
+ public string MenuDescription { get; set; } = string.Empty;
+ public string Target { get; set; } = string.Empty;
+ /// The value must be a JSON string that represents a dictionary of key-value pairs.
+ /// Example: { "name1": "value1", "name2": "value2" }
+ public string DataAttributesJson { get; set; } = string.Empty;
+ public int Order { get; set; }
+ public Type ResourceType { get; set; }
+
+ }
+}
diff --git a/src/cloudscribe.Web.Navigation/Reflection/NavNodeControllerAttribute.cs b/src/cloudscribe.Web.Navigation/Reflection/NavNodeControllerAttribute.cs
new file mode 100644
index 0000000..d58f409
--- /dev/null
+++ b/src/cloudscribe.Web.Navigation/Reflection/NavNodeControllerAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace cloudscribe.Web.Navigation
+{
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public class NavNodeControllerAttribute : Attribute
+ {
+ /// you may set this field and use:
+ /// [NavNode(Key="{Prefix}NodeName", ParentKey="{Prefix}ParentName"]
+ /// then "{Prefix}" be replaced with this field automatically.
+ ///
+ public string KeyPrefix { get; set; } = string.Empty;
+ }
+}
diff --git a/src/cloudscribe.Web.Navigation/Reflection/NavigationTreeReflectionConverter.cs b/src/cloudscribe.Web.Navigation/Reflection/NavigationTreeReflectionConverter.cs
new file mode 100644
index 0000000..55872b9
--- /dev/null
+++ b/src/cloudscribe.Web.Navigation/Reflection/NavigationTreeReflectionConverter.cs
@@ -0,0 +1,341 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+
+namespace cloudscribe.Web.Navigation
+{
+ ///
+ ///
+ public class NavigationTreeReflectionConverter
+ {
+ public async Task> ScanAndMerge(
+ NavigationTreeBuilderService service,
+ string includeAssembliesForScan,
+ TreeNode treeRoot
+ )
+ {
+ return await Task.Run>(() =>
+ {
+ var nodes = Scan(includeAssembliesForScan);
+ treeRoot = Merge(nodes, treeRoot);
+ return treeRoot;
+ });
+ }
+
+ private class NavigationNodeWithParent
+ {
+ public NavigationNode Node { get; set; }
+ public string ParentKey { get; set; }
+ }
+
+ private IList Scan(string includeAssembliesForScan)
+ {
+ var list = new List();
+ // scan and collect NavNodeAttributes
+ var assemblyNames = includeAssembliesForScan
+ .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(m => m.Trim());
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(m => assemblyNames.Contains(new AssemblyName(m.FullName).Name));
+
+ foreach(var assembly in assemblies)
+ {
+ var types = assembly.GetTypes().Where(m =>
+ typeof(Microsoft.AspNetCore.Mvc.ControllerBase).IsAssignableFrom(m)
+ );
+ foreach(var type in types)
+ {
+ var controllerAttr = type.GetCustomAttribute(typeof(NavNodeControllerAttribute), false)
+ as NavNodeControllerAttribute;
+ var areaAttr = type.GetCustomAttribute(typeof(AreaAttribute), true)
+ as AreaAttribute;
+ var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => x.GetCustomAttributes(typeof(NavNodeAttribute), true).Any());
+ foreach(var method in methods)
+ {
+ var attribute = method.GetCustomAttribute(typeof(NavNodeAttribute), true)
+ as NavNodeAttribute;
+ if (attribute != null)
+ {
+ var node = ConvertMvc(type, method, attribute, controllerAttr, areaAttr);
+ list.Add(node);
+ }
+ }
+ }
+
+ //TODO razor pages
+ //var types2 = assembly.GetTypes().Where(m =>
+ // typeof(Microsoft.AspNetCore.Mvc.RazorPages.PageModel).IsAssignableFrom(m)
+ // );
+ //foreach (var type in types2)
+ //{
+ // var attribute = type.GetCustomAttribute(typeof(NavNodeAttribute), true)
+ // as NavNodeAttribute;
+ // var areaAttr = type.GetCustomAttribute(typeof(AreaAttribute), false)
+ // as AreaAttribute;
+ // if (attribute != null)
+ // {
+ // var node = ConvertRazorPage(type, attribute, areaAttr);
+ // list.Add(node);
+ // }
+ //}
+ }
+ return list;
+ }
+
+ private NavigationNodeWithParent ConvertMvc(Type type, MethodInfo method,
+ NavNodeAttribute attribute,
+ NavNodeControllerAttribute controllerAttr,
+ AreaAttribute areaAttr)
+ {
+ var node = Convert(attribute);
+ var prefix = "";
+ if (controllerAttr != null) prefix = controllerAttr.KeyPrefix;
+ node.Key = attribute.Key;
+ if (string.IsNullOrEmpty(node.Key))
+ {
+ node.Key = Guid.NewGuid().ToString();
+ }
+ if (!string.IsNullOrEmpty(node.Key) && node.Key.StartsWith("{Prefix}"))
+ {
+ node.Key = prefix + node.Key.Substring("{Prefix}".Length);
+ }
+ var parentKey = attribute.ParentKey;
+ if (!string.IsNullOrEmpty(parentKey) && parentKey.StartsWith("{Prefix}"))
+ {
+ parentKey = prefix + parentKey.Substring("{Prefix}".Length);
+ }
+
+ node.Area = string.Empty;
+ if (areaAttr != null)
+ {
+ node.Area = areaAttr.RouteValue;
+ }
+ if (!string.IsNullOrEmpty(attribute.Area))
+ {
+ node.Area = attribute.Area;
+ }
+
+ if (!string.IsNullOrEmpty(attribute.Url))
+ {
+ node.Url = attribute.Url;
+ }
+ else
+ {
+ node.Controller = type.Name.Substring(0, type.Name.IndexOf("Controller"));
+
+ node.Action = method.Name;
+ var actioNameAttr = method.GetCustomAttributes(typeof(ActionNameAttribute), true)
+ .FirstOrDefault() as ActionNameAttribute;
+ if (actioNameAttr != null)
+ {
+ node.Action = actioNameAttr.Name;
+ }
+ }
+ return new NavigationNodeWithParent() { Node = node, ParentKey = parentKey };
+ }
+
+ //private NavigationNodeWithParent ConvertRazorPage(Type type,
+ // NavNodeAttribute attribute,
+ // AreaAttribute areaAttr)
+ //{
+ // var node = Convert(attribute);
+ // node.Key = attribute.Key;
+ // if (string.IsNullOrEmpty(node.Key))
+ // {
+ // node.Key = Guid.NewGuid().ToString();
+ // }
+ // var parentKey = attribute.ParentKey;
+
+ // node.Area = string.Empty;
+ // if (areaAttr != null)
+ // {
+ // node.Area = areaAttr.RouteValue;
+ // }
+ // if (!string.IsNullOrEmpty(attribute.Area))
+ // {
+ // node.Area = attribute.Area;
+ // }
+
+ // if (!string.IsNullOrEmpty(attribute.Url))
+ // {
+ // node.Url = attribute.Url;
+ // }
+ // else
+ // {
+ // node.Page = attribute.Page;
+ // }
+ // return new NavigationNodeWithParent() { Node = node, ParentKey = parentKey };
+ //}
+
+
+ private NavigationNode Convert(NavNodeAttribute attribute)
+ {
+ var node = new NavigationNode();
+ if (attribute.ResourceType == null)
+ {
+ node.Text = attribute.Text ?? string.Empty;
+ node.Title = attribute.Title ?? string.Empty;
+ node.MenuDescription = attribute.MenuDescription ?? string.Empty;
+ }
+ else
+ {
+ node.Text = GetResourceString(attribute.ResourceType, attribute.Text);
+ node.Title = GetResourceString(attribute.ResourceType, attribute.Title);
+ node.MenuDescription = GetResourceString(attribute.ResourceType, attribute.MenuDescription);
+ }
+ node.NamedRoute = attribute.NamedRoute ?? string.Empty;
+ node.ExcludeFromSearchSiteMap = attribute.ExcludeFromSearchSiteMap;
+ node.HideFromAuthenticated = attribute.HideFromAuthenticated;
+ node.HideFromAnonymous = attribute.HideFromAnonymous;
+ node.PreservedRouteParameters = attribute.PreservedRouteParameters ?? string.Empty;
+ node.ComponentVisibility = attribute.ComponentVisibility ?? string.Empty;
+ node.AuthorizationPolicy = attribute.AuthorizationPolicy ?? string.Empty;
+ node.ViewRoles = attribute.ViewRoles ?? string.Empty;
+ node.CustomData = attribute.CustomData ?? string.Empty;
+ node.IsClickable = attribute.IsClickable;
+ node.IconCssClass = attribute.IconCssClass ?? string.Empty;
+ node.CssClass = attribute.CssClass ?? string.Empty;
+ node.Target = attribute.Target ?? string.Empty;
+ node.Order = attribute.Order;
+ if (!string.IsNullOrEmpty(attribute.DataAttributesJson))
+ {
+ var list = new List();
+ try
+ {
+ var dict = JsonConvert.DeserializeObject>
+ (attribute.DataAttributesJson);
+ foreach(var key in dict.Keys)
+ {
+ var value = dict[key];
+ if (value != null)
+ {
+ var da = new DataAttribute();
+ da.Attribute = key;
+ da.Value = dict[key].ToString();
+ list.Add(da);
+ }
+ }
+ }
+ catch
+ {
+ }
+ node.DataAttributes = list;
+ }
+
+ return node;
+ }
+ private string GetResourceString(Type type, string resourceName)
+ {
+ if (string.IsNullOrEmpty(resourceName)) return string.Empty;
+ var prop = type.GetProperty(resourceName, BindingFlags.Static | BindingFlags.Public);
+ if (prop == null) return resourceName;
+ return prop.GetGetMethod().Invoke(null, null) as string;
+ }
+
+
+ private TreeNode Merge(IList nodes,
+ TreeNode treeRoot)
+ {
+ var nodesAlreadyAdded = new HashSet();
+ #region process the RootNode
+ if (treeRoot == null)
+ {
+ var homeNode = nodes.FirstOrDefault(m => string.IsNullOrEmpty(m.ParentKey)
+ || m.ParentKey == m.Node.Key);
+ if (homeNode == null)
+ {
+ var rootNav = new NavigationNode();
+ rootNav.Key = "RootNode";
+ //rootNav.IsRootNode = true;
+ rootNav.Text = "Missing RootNode";
+ rootNav.Url = "/";
+ treeRoot = new TreeNode(rootNav);
+ }
+ treeRoot = new TreeNode(homeNode.Node);
+ nodesAlreadyAdded.Add(homeNode.Node.Key);
+ }
+ else
+ {
+ foreach(var node in treeRoot.Flatten())
+ {
+ nodesAlreadyAdded.Add(node.Key);
+ }
+ }
+ #endregion
+
+ var sourceNodesByParent = nodes.ToLookup(n => n.ParentKey);
+ var sourceNodes = new List(nodes);
+ var nodesAddedThisIteration = 0;
+ do
+ {
+ nodesAddedThisIteration = 0;
+ foreach (var nodeWithParent in sourceNodes.ToArray())
+ {
+ if (nodesAlreadyAdded.Contains(nodeWithParent.Node.Key))
+ {
+ continue;
+ }
+
+ var parentNode = treeRoot.FindByKey(nodeWithParent.ParentKey);
+ if (parentNode != null)
+ {
+ var currentNode = parentNode.AddChild(nodeWithParent.Node);
+ nodesAlreadyAdded.Add(nodeWithParent.Node.Key);
+ sourceNodes.Remove(nodeWithParent);
+ nodesAddedThisIteration += 1;
+
+ // Add the rest of the tree branch below the current node
+ this.AddDescendantNodes(currentNode, sourceNodes, sourceNodesByParent, nodesAlreadyAdded);
+ }
+ }
+ }
+ while (nodesAddedThisIteration > 0 && sourceNodes.Count > 0);
+
+ return treeRoot;
+ }
+
+ private void AddDescendantNodes(
+ TreeNode currentNode,
+ List sourceNodes,
+ ILookup sourceNodesByParent,
+ HashSet nodesAlreadyAdded)
+ {
+ if (sourceNodes.Count == 0)
+ {
+ return;
+ }
+
+ var children = sourceNodesByParent[currentNode.Value.Key];
+ if (children.Count() == 0)
+ {
+ return;
+ }
+
+ foreach (var child in children)
+ {
+ if (sourceNodes.Count == 0)
+ {
+ return;
+ }
+
+ var childNode = currentNode.AddChild(child.Node);
+ nodesAlreadyAdded.Add(child.Node.Key);
+ sourceNodes.Remove(child);
+
+ if (sourceNodes.Count == 0)
+ {
+ return;
+ }
+
+ this.AddDescendantNodes(childNode, sourceNodes, sourceNodesByParent, nodesAlreadyAdded);
+ }
+ }
+
+ }
+}
diff --git a/src/cloudscribe.Web.Navigation/Reflection/ReflectionNavigationTreeBuilder.cs b/src/cloudscribe.Web.Navigation/Reflection/ReflectionNavigationTreeBuilder.cs
new file mode 100644
index 0000000..8e753e2
--- /dev/null
+++ b/src/cloudscribe.Web.Navigation/Reflection/ReflectionNavigationTreeBuilder.cs
@@ -0,0 +1,78 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace cloudscribe.Web.Navigation
+{
+ ///
+ ///
+ public class ReflectionNavigationTreeBuilder : INavigationTreeBuilder
+ {
+ public ReflectionNavigationTreeBuilder(
+ IWebHostEnvironment appEnv,
+ IOptions navigationOptionsAccessor,
+ IEnumerable treeProcessors,
+ ILogger logger)
+ {
+ if (appEnv == null) { throw new ArgumentNullException(nameof(appEnv)); }
+ if (logger == null) { throw new ArgumentNullException(nameof(logger)); }
+ if (navigationOptionsAccessor == null) { throw new ArgumentNullException(nameof(navigationOptionsAccessor)); }
+
+ _env = appEnv;
+ _navOptions = navigationOptionsAccessor.Value;
+ _treeProcessors = treeProcessors;
+ _log = logger;
+
+ }
+
+ private readonly IWebHostEnvironment _env;
+ private readonly NavigationOptions _navOptions;
+ private readonly ILogger _log;
+ private TreeNode rootNode = null;
+ private readonly IEnumerable _treeProcessors;
+
+ public string Name
+ {
+ get { return Constants.ReflectionNavigationTreeBuilderName; }
+ }
+
+ public async Task> BuildTree(NavigationTreeBuilderService service)
+ {
+ if (rootNode == null)
+ {
+ rootNode = await BuildTreeInternal(service);
+ foreach (var processor in _treeProcessors)
+ {
+ await processor.ProcessTree(rootNode);
+ }
+ }
+
+ return rootNode;
+ }
+
+ private async Task> BuildTreeInternal(NavigationTreeBuilderService service)
+ {
+
+ if (string.IsNullOrEmpty(_navOptions.IncludeAssembliesForScan))
+ {
+ _log.LogError("unable to build navigation tree, 'IncludeAssembliesForScan' not specified. ");
+
+ var rootNav = new NavigationNode();
+ rootNav.Key = "RootNode";
+ //rootNav.IsRootNode = true;
+ rootNav.Text = "Missing config for IncludeAssembliesForScan";
+ rootNav.Url = "/";
+ var treeRoot = new TreeNode(rootNav);
+
+ return treeRoot;
+ }
+
+ var helper = new NavigationTreeReflectionConverter();
+ return await helper.ScanAndMerge(service, _navOptions.IncludeAssembliesForScan, null).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/cloudscribe.Web.Navigation/ServiceCollectionExtensions.cs b/src/cloudscribe.Web.Navigation/ServiceCollectionExtensions.cs
index c581e40..caca1f5 100644
--- a/src/cloudscribe.Web.Navigation/ServiceCollectionExtensions.cs
+++ b/src/cloudscribe.Web.Navigation/ServiceCollectionExtensions.cs
@@ -33,8 +33,9 @@ public static IServiceCollection AddCloudscribeNavigation(
services.AddDistributedMemoryCache();
services.TryAddScoped();
-
- services.TryAddScoped();
+
+ services.TryAddScoped();
+ services.AddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
diff --git a/src/cloudscribe.Web.Navigation/TreeBuilders/JsonNavigationTreeBuilder.cs b/src/cloudscribe.Web.Navigation/TreeBuilders/JsonNavigationTreeBuilder.cs
index b81b525..b57bd35 100644
--- a/src/cloudscribe.Web.Navigation/TreeBuilders/JsonNavigationTreeBuilder.cs
+++ b/src/cloudscribe.Web.Navigation/TreeBuilders/JsonNavigationTreeBuilder.cs
@@ -43,7 +43,7 @@ ILogger logger
public string Name
{
- get { return "cloudscribe.Web.Navigation.JsonNavigationTreeBuilder"; }
+ get { return Constants.JsonNavigationTreeBuilderName; }
}
public async Task> BuildTree(NavigationTreeBuilderService service)
diff --git a/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeJsonConverter.cs b/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeJsonConverter.cs
index b0e949e..fd7a84a 100644
--- a/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeJsonConverter.cs
+++ b/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeJsonConverter.cs
@@ -45,14 +45,18 @@ private TreeNode CreateTreeNode(TreeNode tNode,
if(jNode["Value"]["Key"] != null)
{
navNode.Key = (string)jNode["Value"]["Key"];
- }
-
- //if(jNode["Value"]["ParentKey"] != null)
- //{
- // navNode.ParentKey = (string)jNode["Value"]["ParentKey"];
- //}
-
- if(jNode["Value"]["Controller"] != null)
+ }
+ if (string.IsNullOrEmpty(navNode.Key))
+ {
+ navNode.Key = Guid.NewGuid().ToString();
+ }
+
+ //if(jNode["Value"]["ParentKey"] != null)
+ //{
+ // navNode.ParentKey = (string)jNode["Value"]["ParentKey"];
+ //}
+
+ if (jNode["Value"]["Controller"] != null)
{
navNode.Controller = (string)jNode["Value"]["Controller"];
}
@@ -151,11 +155,16 @@ private TreeNode CreateTreeNode(TreeNode tNode,
if (jNode["Value"]["CssClass"] != null)
{
navNode.CssClass = (string)jNode["Value"]["CssClass"];
- }
-
- //TODO: add DataAttributes collection
-
-
+ }
+
+ if (jNode["Value"]["Order"] != null)
+ {
+ navNode.Order = Convert.ToInt32((string)jNode["Value"]["Order"]);
+ }
+
+ //TODO: add DataAttributes collection
+
+
if (tNode == null)
{
TreeNode rootNode = new TreeNode(navNode);
diff --git a/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeXmlConverter.cs b/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeXmlConverter.cs
index 7c265a9..0957f46 100644
--- a/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeXmlConverter.cs
+++ b/src/cloudscribe.Web.Navigation/TreeBuilders/NavigationTreeXmlConverter.cs
@@ -6,6 +6,7 @@
//
using System;
+using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
@@ -371,6 +372,11 @@ NavigationTreeBuilderService service
var a = xmlNode.Attribute("key");
if(a != null) { navNode.Key = a.Value; }
+ // automatically generate a key if
+ if (string.IsNullOrEmpty(navNode.Key))
+ {
+ navNode.Key = Guid.NewGuid().ToString();
+ }
//a = xmlNode.Attribute("parentKey");
//if (a != null) { navNode.ParentKey = a.Value; }
@@ -392,10 +398,10 @@ NavigationTreeBuilderService service
if (a != null) { navNode.NamedRoute = a.Value; }
a = xmlNode.Attribute("text");
- if (a != null) { navNode.Text = a.Value; }
+ if (a != null) { navNode.Text = ProcessResourceString(a.Value); }
a = xmlNode.Attribute("title");
- if (a != null) { navNode.Title = a.Value; }
+ if (a != null) { navNode.Title = ProcessResourceString(a.Value); }
a = xmlNode.Attribute("url");
if (a != null) { navNode.Url = a.Value; }
@@ -456,10 +462,13 @@ NavigationTreeBuilderService service
if (a != null) { navNode.CssClass = a.Value; }
a = xmlNode.Attribute("menuDescription");
- if (a != null) { navNode.MenuDescription = a.Value; }
+ if (a != null) { navNode.MenuDescription = ProcessResourceString(a.Value); }
a = xmlNode.Attribute("target");
- if (a != null) { navNode.Target = a.Value; }
+ if (a != null) { navNode.Target = a.Value; }
+
+ a = xmlNode.Attribute("order");
+ if (a != null) { navNode.Order = Convert.ToInt32(a.Value); }
var da = xmlNode.Element(XName.Get("DataAttributes"));
if (da != null)
@@ -482,10 +491,67 @@ NavigationTreeBuilderService service
}
-
-
return navNode;
- }
-
+ }
+
+ ///
+ /// support for the old resource string in ASP.NET Site Navigation, see:
+ /// https://docs.microsoft.com/en-us/previous-versions/aspnet/ms178427(v=vs.100)?redirectedfrom=MSDN
+ ///
+ /// Only use this for compatiblity with MvcSiteMapProvider.
+ ///
+ /// format: $resources:ClassName,ResourceName,DefaultString (DefaultString is optional)
+ ///
+ private string ProcessResourceString(string value)
+ {
+ if (!string.IsNullOrEmpty(value) && value.Length > 11)
+ {
+ var tmp = value.Trim();
+ if (tmp.ToLowerInvariant().StartsWith("$resources:"))
+ {
+ tmp = tmp.Substring(11);
+ var pieces = tmp.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ if (pieces.Length >= 2)
+ {
+ var className = pieces[0].Trim();
+ var resourceName = pieces[1].Trim();
+ var defaultString = (pieces.Length >= 3 ? pieces[2] : resourceName);
+ var type = GetTypeFromAppDomain(className);
+ if (type == null) return defaultString;
+ var prop = type.GetProperty(resourceName,
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
+ if (prop == null) return defaultString;
+ return prop.GetGetMethod().Invoke(null, null) as string;
+ }
+ }
+ }
+ return value;
+ }
+ private Dictionary classesDict;
+ private Type GetTypeFromAppDomain(string className)
+ {
+ if (classesDict == null) classesDict = new Dictionary();
+ Type type = null;
+ if (classesDict.ContainsKey(className))
+ {
+ type = classesDict[className];
+ }
+ else
+ {
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ foreach (var assembly in assemblies)
+ {
+ type = assembly.GetType(className);
+ if (type != null)
+ {
+ classesDict[className] = type;
+ return type;
+ }
+ }
+ classesDict[className] = null; //not found
+ }
+ return type;
+ }
}
+
}
diff --git a/src/cloudscribe.Web.Navigation/TreeBuilders/XmlNavigationTreeBuilder.cs b/src/cloudscribe.Web.Navigation/TreeBuilders/XmlNavigationTreeBuilder.cs
index b84029f..dfc6418 100644
--- a/src/cloudscribe.Web.Navigation/TreeBuilders/XmlNavigationTreeBuilder.cs
+++ b/src/cloudscribe.Web.Navigation/TreeBuilders/XmlNavigationTreeBuilder.cs
@@ -43,7 +43,7 @@ public XmlNavigationTreeBuilder(
public string Name
{
- get { return "cloudscribe.Web.Navigation.XmlNavigationTreeBuilder"; }
+ get { return Constants.XmlNavigationTreeBuilderName; }
}
public async Task> BuildTree(
diff --git a/src/cloudscribe.Web.Navigation/TreeNode.cs b/src/cloudscribe.Web.Navigation/TreeNode.cs
index bd2fa68..4627aff 100644
--- a/src/cloudscribe.Web.Navigation/TreeNode.cs
+++ b/src/cloudscribe.Web.Navigation/TreeNode.cs
@@ -172,6 +172,28 @@ public TreeNode GetParent()
parentNode.ParentValueChain.RemoveAt(parentChainCount - 1);
return parentNode;
- }
+ }
+
+ private class TreeNodeComparer : IComparer>
+ {
+ public int Compare([AllowNull] TreeNode x, [AllowNull] TreeNode y)
+ {
+ if (x.Value is NavigationNode thisNode
+ && y.Value is NavigationNode otherNode)
+ {
+ return thisNode.Order.CompareTo(otherNode.Order);
+ }
+ return 0;
+ }
+ }
+ public void Sort()
+ {
+ // List.Sort is unstable sort, which would probably swap items with the same order.
+ // LINQ OrderBy is stable sort, which tries to preserve the original order.
+
+ var tmp = _children.OrderBy(x => x, new TreeNodeComparer()).ToArray();
+ _children.Clear();
+ _children.AddRange(tmp);
+ }
}
}