Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 100 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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]

[![Twitter URL](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/cloudscribeweb) [![Twitter Follow](https://img.shields.io/twitter/follow/cloudscribeweb.svg?style=social&label=Follow)](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 |
| ------------- | ------------- |
| [![Build status](https://ci.appveyor.com/api/projects/status/ghcdgoi7hybt8mue?svg=true)](https://ci.appveyor.com/project/joeaudette/cloudscribe-web-navigation) | [![Build Status](https://travis-ci.org/cloudscribe/cloudscribe.Web.Navigation.svg?branch=master)](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.
2 changes: 2 additions & 0 deletions cloudscribe.Web.Navigation.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/MvcSitemapConvert/MvcSitemapConvert.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

</Project>
128 changes: 128 additions & 0 deletions src/MvcSitemapConvert/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@{
ViewData["Title"] = "Reflection";
}

<h2>Reflection Index (in Area 51)</h2>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@{
ViewData["Title"] = "Mulan";
}

<h2>Mulan (in Area 51)</h2>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@{
ViewData["Title"] = "Mushu";
}

<h2>Mushu (in Area 51)</h2>
4 changes: 3 additions & 1 deletion src/NavigationDemo.Web/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
35 changes: 35 additions & 0 deletions src/NavigationDemo.Web/Controllers/ReflectionController.cs
Original file line number Diff line number Diff line change
@@ -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();
}


}
}
15 changes: 15 additions & 0 deletions src/NavigationDemo.Web/NavigationDemo.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@
<Folder Include="wwwroot\lib\" />
</ItemGroup>

<ItemGroup>
<Compile Update="Resources\MyResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>MyResource.resx</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Resources\MyResource.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>MyResource.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>




Expand Down
Loading