Implementing a navigation menu system for admin pages.

--HG--
extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4039716
This commit is contained in:
loudej
2009-11-11 23:36:29 +00:00
parent 3a951297fa
commit d5a78a53ed
31 changed files with 754 additions and 41 deletions

View File

@@ -39,6 +39,7 @@ namespace Orchard.Tests.Packages.Users.Services {
var databaseFileName = System.IO.Path.GetTempFileName();
_sessionFactory = DataUtility.CreateSessionFactory(
databaseFileName,
typeof(UserRecord),
typeof(ModelRecord),
typeof(ModelTypeRecord));
}

View File

@@ -138,6 +138,9 @@
<Compile Include="Records\Foo.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Stubs\StubHttpContext.cs" />
<Compile Include="UI\Navigation\MenuItemComparerTests.cs" />
<Compile Include="UI\Navigation\NavigationManagerTests.cs" />
<Compile Include="UI\Navigation\PositionComparerTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Orchard\Orchard.csproj">

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
using NUnit.Framework;
using Orchard.UI.Navigation;
namespace Orchard.Tests.UI.Navigation {
[TestFixture]
public class MenuItemComparerTests {
[Test]
public void TextShouldCauseDifferenceAndNullRouteValuesAreEqual() {
var item1 = new MenuItem { Text = "hello" };
var item2 = new MenuItem { Text = "hello" };
var item3 = new MenuItem { Text = "hello3" };
AssertSameSameDifferent(item1, item2, item3);
}
[Test]
public void NullRouteValuesShouldNotEqualEmptyRouteValues() {
var item1 = new MenuItem { Text = "hello" };
var item2 = new MenuItem { Text = "hello" };
var item3 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary() };
var item4 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary() };
AssertSameSameDifferent(item1, item2, item3);
AssertSameSameDifferent(item3, item4, item1);
}
[Test]
public void AdditionalPropertiesShouldMismatch() {
var item1 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = 1 }) };
var item2 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = 1 }) };
var item3 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = 1, two = 2 }) };
AssertSameSameDifferent(item1, item2, item3);
}
[Test]
public void ValueTypeShouldMismatch() {
var item1 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = 1 }) };
var item2 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = 1 }) };
var item3 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1" }) };
AssertSameSameDifferent(item1, item2, item3);
}
[Test]
public void ValuesShouldMismatch() {
var item1 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "2" }) };
var item2 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "2" }) };
var item3 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "3" }) };
AssertSameSameDifferent(item1, item2, item3);
}
[Test]
public void PositionAndChildrenDontMatter() {
var item1 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "2" }) };
var item2 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "2" }), Position = "4.0" };
var item3 = new MenuItem { Text = "hello", RouteValues = new RouteValueDictionary(new { one = "1", two = "2" }), Contained = new[] { new MenuItem() } };
AssertSameSameSame(item1, item2, item3);
}
private static void AssertSameSameDifferent(MenuItem item1, MenuItem item2, MenuItem item3) {
var comparer = new MenuItemComparer();
Assert.That(comparer.Equals(item1, item2), Is.True);
Assert.That(comparer.Equals(item1, item3), Is.False);
Assert.That(comparer.Equals(item2, item3), Is.False);
Assert.That(comparer.GetHashCode(item1), Is.EqualTo(comparer.GetHashCode(item2)));
// - hash inequality isn't really guaranteed, now that you mention it
//Assert.That(comparer.GetHashCode(item1), Is.Not.EqualTo(comparer.GetHashCode(item3)));
//Assert.That(comparer.GetHashCode(item2), Is.Not.EqualTo(comparer.GetHashCode(item3)));
}
private static void AssertSameSameSame(MenuItem item1, MenuItem item2, MenuItem item3) {
var comparer = new MenuItemComparer();
Assert.That(comparer.Equals(item1, item2), Is.True);
Assert.That(comparer.Equals(item1, item3), Is.True);
Assert.That(comparer.Equals(item2, item3), Is.True);
Assert.That(comparer.GetHashCode(item1), Is.EqualTo(comparer.GetHashCode(item2)));
Assert.That(comparer.GetHashCode(item1), Is.EqualTo(comparer.GetHashCode(item3)));
Assert.That(comparer.GetHashCode(item2), Is.EqualTo(comparer.GetHashCode(item3)));
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Orchard.UI.Navigation;
namespace Orchard.Tests.UI.Navigation {
[TestFixture]
public class NavigationManagerTests {
[Test]
public void EmptyMenuIfNameDoesntMatch() {
var manager = new NavigationManager(new[] { new StubProvider() });
var menuItems = manager.BuildMenu("primary");
Assert.That(menuItems.Count(), Is.EqualTo(0));
}
[Test]
public void NavigationManagerShouldUseProvidersToBuildNamedMenu() {
var manager = new NavigationManager(new[] { new StubProvider() });
var menuItems = manager.BuildMenu("admin");
Assert.That(menuItems.Count(), Is.EqualTo(2));
Assert.That(menuItems.First(), Has.Property("Text").EqualTo("Foo"));
Assert.That(menuItems.Last(), Has.Property("Text").EqualTo("Bar"));
Assert.That(menuItems.Last().Contained.Count(), Is.EqualTo(1));
Assert.That(menuItems.Last().Contained.Single().Text, Is.EqualTo("Frap"));
}
[Test]
public void NavigationManagerShouldMergeAndOrderNavigation() {
var manager = new NavigationManager(new INavigationProvider[] { new StubProvider(), new Stub2Provider() });
var menuItems = manager.BuildMenu("admin");
Assert.That(menuItems.Count(), Is.EqualTo(3));
var item1 = menuItems.First();
var item2 = menuItems.Skip(1).First();
var item3 = menuItems.Skip(2).First();
Assert.That(item1.Text, Is.EqualTo("Foo"));
Assert.That(item1.Position, Is.EqualTo("1.0"));
Assert.That(item2.Text, Is.EqualTo("Bar"));
Assert.That(item2.Position, Is.EqualTo("2.0"));
Assert.That(item3.Text, Is.EqualTo("Frap"));
Assert.That(item3.Position, Is.EqualTo("3.0"));
Assert.That(item2.Contained.Count(), Is.EqualTo(2));
var subitem1 = item2.Contained.First();
var subitem2 = item2.Contained.Last();
Assert.That(subitem1.Text, Is.EqualTo("Quad"));
Assert.That(subitem1.Position, Is.EqualTo("1.a"));
Assert.That(subitem2.Text, Is.EqualTo("Frap"));
Assert.That(subitem2.Position, Is.EqualTo("1.b"));
}
public class StubProvider : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder
.Add("Foo", "1.0")
.Add("Bar", "2.0", x => x.Add("Frap", "1.b"));
}
}
public class Stub2Provider : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder
.Add("Frap", "3.0")
.Add("Bar", "4.0", x => x.Add("Quad", "1.a"));
}
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Orchard.UI.Navigation;
namespace Orchard.Tests.UI.Navigation {
[TestFixture]
public class PositionComparerTests {
private IComparer<string> _comparer;
[SetUp]
public void Init() {
_comparer = new PositionComparer();
}
[Test]
public void LessThanAndGreaterThanShouldBeBelowAndAboveZero() {
var lessThan = StringComparer.InvariantCultureIgnoreCase.Compare("alpha", "beta");
var greaterThan = StringComparer.InvariantCultureIgnoreCase.Compare("gamma", "delta");
Assert.That(lessThan, Is.LessThan(0));
Assert.That(greaterThan, Is.GreaterThan(0));
}
[Test]
public void NullIsLessThanEmptyAndEmptyIsLessThanNonEmpty() {
Assert.That(_comparer.Compare(null, ""), Is.LessThan(0));
Assert.That(_comparer.Compare("", "5"), Is.LessThan(0));
Assert.That(_comparer.Compare(null, "5"), Is.LessThan(0));
Assert.That(_comparer.Compare("", null), Is.GreaterThan(0));
Assert.That(_comparer.Compare("5", ""), Is.GreaterThan(0));
Assert.That(_comparer.Compare("5", null), Is.GreaterThan(0));
Assert.That(_comparer.Compare(null, null), Is.EqualTo(0));
Assert.That(_comparer.Compare("", ""), Is.EqualTo(0));
}
[Test]
public void NumericValuesShouldCompareNumerically() {
AssertLess("3", "5");
AssertMore("8", "5");
AssertSame("5", "5");
AssertMore("100", "5");
AssertSame("007", "7");
}
[Test]
public void DotsSplitParts() {
AssertLess("0500.3", "0500.5");
AssertMore("0500.8", "0500.5");
AssertSame("0500.5", "0500.5");
AssertMore("0500.100", "0500.5");
AssertSame("0500.007", "0500.7");
AssertLess("0500.3.0300", "0500.5.0300");
AssertMore("0500.8.0300", "0500.5.0300");
AssertSame("0500.5.0300", "0500.5.0300");
AssertMore("0500.100.0300", "0500.5.0300");
AssertSame("0500.007.0300", "0500.7.0300");
}
[Test]
public void NumericValuesShouldComeBeforeNonNumeric() {
AssertLess("5", "x");
AssertLess("50", "50a");
AssertLess("1.50", "1.50a");
}
[Test]
public void NonNumericValuesCompareOrdinallyAndIgnoreCase() {
AssertSame("x", "X");
AssertLess("rt675x", "rt685x");
AssertMore("ru675x", "rt675x");
AssertLess("rt675x", "rt675y");
AssertSame("1.x.5", "1.X.5");
AssertLess("1.rt675x.5", "1.rt685x.5");
AssertMore("1.ru675x.5", "1.rt675x.5");
AssertLess("1.rt675x.5", "1.rt675y.5");
AssertSame("x.5", "X.5");
AssertLess("rt675x.5", "rt685x.5");
AssertMore("ru675x.5", "rt675x.5");
AssertLess("rt675x.5", "rt675y.5");
AssertSame("1.x", "1.X");
AssertLess("1.rt675x", "1.rt685x");
AssertMore("1.ru675x", "1.rt675x");
AssertLess("1.rt675x", "1.rt675y");
}
[Test]
public void LongerNonNumericShouldComeLater() {
AssertLess("rt675x", "rt675xx");
}
[Test]
public void EmptyBitsAreSafeAndShouldComeFirst() {
AssertSame("1.2.3", "1.2.3");
AssertSame(".1.2.3.", ".1.2.3.");
AssertSame(".1..3.", ".1..3.");
AssertLess("1..3", "1.2.3");
AssertLess(".1..3.", ".1.2.3.");
AssertSame("a.b.c", "a.b.c");
AssertSame(".a.b.c.", ".a.b.c.");
AssertSame(".a..c.", ".a..c.");
AssertLess("a..c", "a.b.c");
AssertLess(".a..c.", ".a.b.c.");
}
[Test]
public void AdditionalNonEmptySegmentsShouldComeLater() {
AssertLess("1.2", "1.2.3");
AssertSame("1.2", "1.2.");
AssertLess("a.b", "a.b.c");
AssertSame("a.b", "a.b.");
}
void AssertLess(string x, string y) {
Assert.That(_comparer.Compare(x, y), Is.LessThan(0));
Assert.That(_comparer.Compare(y, x), Is.GreaterThan(0));
}
void AssertMore(string x, string y) {
Assert.That(_comparer.Compare(x, y), Is.GreaterThan(0));
Assert.That(_comparer.Compare(y, x), Is.LessThan(0));
}
void AssertSame(string x, string y) {
Assert.That(_comparer.Compare(x, y), Is.EqualTo(0));
Assert.That(_comparer.Compare(y, x), Is.EqualTo(0));
}
}
}

View File

@@ -0,0 +1,15 @@
using Orchard.UI.Navigation;
namespace Orchard.CmsPages {
public class AdminMenu : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add("Pages", "1",
menu => menu
.Add("Manage Pages", "1.0", item => item.Action("Index", "Admin", new { area = "Orchard.CmsPages" }))
.Add("Add a Page", "1.1", item => item.Action("Create", "Admin", new { area = "Orchard.CmsPages" }))
);
}
}
}

View File

@@ -72,6 +72,7 @@
<Reference Include="System.Web.Mobile" />
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\TemplatesController.cs" />
<Compile Include="Models\ContentItem.cs" />
@@ -175,7 +176,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>31696</DevelopmentServerPort>
<DevelopmentServerPort>65251</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -111,7 +111,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>18755</DevelopmentServerPort>
<DevelopmentServerPort>65263</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -0,0 +1,14 @@
using Orchard.UI.Navigation;
namespace Orchard.Media {
public class AdminMenu : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add("Media", "4",
menu => menu
.Add("Manage Folders", "1.0", item => item.Action("Index", "Admin", new { area = "Orchard.Media" }))
);
}
}
}

View File

@@ -73,6 +73,7 @@
<Reference Include="System.Web.Mobile" />
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Helpers\MediaHelpers.cs" />
<Compile Include="Models\FolderNavigation.cs" />
@@ -142,7 +143,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>18754</DevelopmentServerPort>
<DevelopmentServerPort>65259</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -0,0 +1,14 @@
using Orchard.UI.Navigation;
namespace Orchard.Roles {
public class AdminMenu : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add("Users", "5",
menu => menu
.Add("Manage Roles", "2.0", item => item.Action("Index", "Admin", new { area = "Orchard.Roles" }))
.Add("Create a Role", "2.1", item => item.Action("Create", "Admin", new { area = "Orchard.Roles" })));
}
}
}

View File

@@ -61,6 +61,7 @@
<Reference Include="System.Web.Mobile" />
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Models\PermissionRecord.cs" />
<Compile Include="Models\RoleRecord.cs" />

View File

@@ -0,0 +1,14 @@
using Orchard.UI.Navigation;
namespace Orchard.Users {
public class AdminMenu : INavigationProvider {
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add("Users", "5",
menu => menu
.Add("Manage Users", "1.0", item => item.Action("Index", "Admin", new { area = "Orchard.Users" }))
.Add("Create New User", "1.1", item => item.Action("Create", "Admin", new { area = "Orchard.Users" })));
}
}
}

View File

@@ -68,6 +68,7 @@
<Compile Include="Models\UserRecord.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\MembershipService.cs" />
<Compile Include="AdminMenu.cs" />
<Compile Include="ViewModels\UserCreateViewModel.cs" />
<Compile Include="ViewModels\UserEditViewModel.cs" />
<Compile Include="ViewModels\UsersIndexViewModel.cs" />
@@ -125,7 +126,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>40373</DevelopmentServerPort>
<DevelopmentServerPort>65271</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -14,7 +14,7 @@
<h2 class="separator">
Edit User</h2>
<p class="bottomSpacer">
<%=Html.ActionLink("Users", "Index") %> > Edit #<%=Model.Id%> <strong><%=Html.Encode(Model.UserName)%></strong>
<%=Html.ActionLink("Users", "Index") %> > Edit #<%= Model.Id%> <strong><%=Html.Encode(Model.UserName)%></strong>
</p>
</div>
<div class="yui-u">

View File

@@ -107,7 +107,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>15061</DevelopmentServerPort>
<DevelopmentServerPort>65267</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -155,7 +155,7 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>26570</DevelopmentServerPort>
<DevelopmentServerPort>65255</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>

View File

@@ -1,4 +1,5 @@
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<AdminViewModel>" %>
<%@ Import Namespace="Orchard.Mvc.ViewModels"%>
</div><%-- yui-b --%>
@@ -9,31 +10,15 @@
<h4>
Dashboard</h4>
</div>
<div class="leftNavMod">
<h4>
Pages</h4>
<ul>
<li><%=Html.ActionLink("Manage Pages", "Index", new {area="Orchard.CmsPages",controller="Admin"}) %></li>
<li><%=Html.ActionLink("Add a Page", "Create", new {area="Orchard.CmsPages",controller="Admin"}) %></li>
</ul>
</div>
<div class="leftNavMod">
<h4>
Media</h4>
<ul>
<li><%=Html.ActionLink("Manage Folders", "Index", new {area="Orchard.Media",controller="Admin"}) %></li>
</ul>
</div>
<div class="leftNavMod">
<h4>
Users</h4>
<ul>
<li><%=Html.ActionLink("Manage Roles", "Index", new {area="Orchard.Roles",controller="Admin"}) %></li>
</ul>
<ul>
<li><%=Html.ActionLink("Add a Role", "Create", new {area="Orchard.Roles",controller="Admin"}) %></li>
</ul>
</div>
<%if (Model.AdminMenu != null) {
foreach (var menuSection in Model.AdminMenu) {%>
<div class="leftNavMod"><h4><%=Html.Encode(menuSection.Text)%></h4><ul><%foreach (var menuItem in menuSection.Contained) { %>
<li><%=Html.ActionLink(menuItem.Text, (string)menuItem.RouteValues["action"], menuItem.RouteValues)%></li>
<%} %></ul></div>
<%
}
}%>
</div>
</div><%-- bd --%>

View File

@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using Orchard.UI.Navigation;
namespace Orchard.Mvc.ViewModels {
public class AdminViewModel : BaseViewModel {
public IEnumerable<MenuItem> AdminMenu { get; set; }
}
}

View File

@@ -158,10 +158,17 @@
<Compile Include="Mvc\Routes\RouteExtensions.cs" />
<Compile Include="Mvc\ViewModels\AdminViewModel.cs" />
<Compile Include="Mvc\ViewModels\BaseViewModel.cs" />
<Compile Include="Notify\Notifier.cs" />
<Compile Include="Notify\NotifierExtensions.cs" />
<Compile Include="Notify\NotifyEntry.cs" />
<Compile Include="Notify\NotifyFilter.cs" />
<Compile Include="UI\Menus\AdminMenuFilter.cs" />
<Compile Include="UI\Navigation\INavigationBuilder.cs" />
<Compile Include="UI\Navigation\INavigationProvider.cs" />
<Compile Include="UI\Navigation\MenuItem.cs" />
<Compile Include="UI\Navigation\MenuItemComparer.cs" />
<Compile Include="UI\Navigation\NavigationManager.cs" />
<Compile Include="UI\Navigation\PositionComparer.cs" />
<Compile Include="UI\Notify\Notifier.cs" />
<Compile Include="UI\Notify\NotifierExtensions.cs" />
<Compile Include="UI\Notify\NotifyEntry.cs" />
<Compile Include="UI\Notify\NotifyFilter.cs" />
<Compile Include="Packages\PackageEntry.cs" />
<Compile Include="Packages\PackageFolders.cs" />
<Compile Include="Packages\PackageDescriptor.cs" />

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Orchard.Mvc.Filters;
using Orchard.Mvc.ViewModels;
using Orchard.UI.Navigation;
namespace Orchard.UI.Menus {
public class AdminMenuFilter : FilterProvider, IResultFilter {
private readonly INavigationManager _navigationManager;
public AdminMenuFilter(INavigationManager navigationManager ) {
_navigationManager = navigationManager;
}
public void OnResultExecuting(ResultExecutingContext filterContext) {
var viewResult = filterContext.Result as ViewResult;
if (viewResult == null)
return;
var adminViewModel = viewResult.ViewData.Model as AdminViewModel;
if (adminViewModel == null)
return;
adminViewModel.AdminMenu = _navigationManager.BuildMenu("admin");
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
namespace Orchard.UI.Navigation {
public class NavigationBuilder {
IEnumerable<MenuItem> Contained { get; set; }
public NavigationBuilder Add(string caption, string position, Action<NavigationItemBuilder> itemBuilder) {
var childBuilder = new NavigationItemBuilder();
if (!string.IsNullOrEmpty(caption))
childBuilder.Caption(caption);
if (!string.IsNullOrEmpty(position))
childBuilder.Position(position);
itemBuilder(childBuilder);
Contained = (Contained ?? Enumerable.Empty<MenuItem>()).Concat(childBuilder.Build());
return this;
}
public NavigationBuilder Add(string caption, Action<NavigationItemBuilder> itemBuilder) {
return Add(caption, null, itemBuilder);
}
public NavigationBuilder Add(Action<NavigationItemBuilder> itemBuilder) {
return Add(null, null, itemBuilder);
}
public NavigationBuilder Add(string caption, string position) {
return Add(caption, position, x=> { });
}
public NavigationBuilder Add(string caption) {
return Add(caption, null, x => { });
}
public IEnumerable<MenuItem> Build() {
return (Contained ?? Enumerable.Empty<MenuItem>()).ToList();
}
}
public class NavigationItemBuilder : NavigationBuilder {
private readonly MenuItem _item;
public NavigationItemBuilder() {
_item = new MenuItem();
}
public NavigationItemBuilder Caption(string caption) {
_item.Text = caption;
return this;
}
public NavigationItemBuilder Position(string position) {
_item.Position = position;
return this;
}
public new IEnumerable<MenuItem> Build() {
_item.Contained = base.Build();
return new[] { _item };
}
public NavigationItemBuilder Action(string actionName) {
return Action(actionName, null, new RouteValueDictionary());
}
public NavigationItemBuilder Action(string actionName, string controllerName) {
return Action(actionName, controllerName, new RouteValueDictionary());
}
public NavigationItemBuilder Action(string actionName, string controllerName, object values) {
return Action(actionName, controllerName, new RouteValueDictionary(values));
}
public NavigationItemBuilder Action(string actionName, string controllerName, RouteValueDictionary values) {
_item.RouteValues = new RouteValueDictionary(values);
if (!string.IsNullOrEmpty(actionName))
_item.RouteValues["action"] = actionName;
if (!string.IsNullOrEmpty(controllerName))
_item.RouteValues["controller"] = controllerName;
return this;
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Orchard.UI.Navigation {
public interface INavigationProvider : IDependency {
string MenuName { get; }
void GetNavigation(NavigationBuilder builder);
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Web.Routing;
namespace Orchard.UI.Navigation {
public class MenuItem {
public string Text { get; set; }
public string Position { get; set; }
public RouteValueDictionary RouteValues { get; set; }
public IEnumerable<MenuItem> Contained { get; set; }
}
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
namespace Orchard.UI.Navigation {
public class MenuItemComparer : IEqualityComparer<MenuItem> {
public bool Equals(MenuItem x, MenuItem y) {
if (!string.Equals(x.Text, y.Text)) {
return false;
}
if (x.RouteValues != null || y.RouteValues != null) {
if (x.RouteValues == null || y.RouteValues == null) {
return false;
}
if (x.RouteValues.Keys.Any(key => y.RouteValues.ContainsKey(key) == false)) {
return false;
}
if (y.RouteValues.Keys.Any(key => x.RouteValues.ContainsKey(key) == false)) {
return false;
}
foreach (var key in x.RouteValues.Keys) {
if (!object.Equals(x.RouteValues[key], y.RouteValues[key])) {
return false;
}
}
}
return true;
}
public int GetHashCode(MenuItem obj) {
var hash = 0;
if (obj.Text != null) {
hash ^= obj.Text.GetHashCode();
}
if (obj.RouteValues != null) {
foreach (var item in obj.RouteValues) {
hash ^= item.Key.GetHashCode() ^ item.Value.GetHashCode();
}
}
return hash;
}
}
}

View File

@@ -0,0 +1,65 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace Orchard.UI.Navigation {
public interface INavigationManager : IDependency {
IEnumerable<MenuItem> BuildMenu(string menuName);
}
public class NavigationManager : INavigationManager {
private readonly IEnumerable<INavigationProvider> _providers;
public NavigationManager(IEnumerable<INavigationProvider> providers) {
_providers = providers;
}
public IEnumerable<MenuItem> BuildMenu(string menuName) {
return Merge(AllSources(menuName)).ToArray();
}
private IEnumerable<IEnumerable<MenuItem>> AllSources(string menuName) {
foreach (var provider in _providers) {
if (provider.MenuName == menuName) {
var builder = new NavigationBuilder();
provider.GetNavigation(builder);
yield return builder.Build();
}
}
}
private static IEnumerable<MenuItem> Merge(IEnumerable<IEnumerable<MenuItem>> sources) {
var comparer = new MenuItemComparer();
var orderer = new PositionComparer();
return sources.SelectMany(x => x).ToArray()
.GroupBy(key => key, (key, items) => Join(items), comparer)
.OrderBy(item => item.Position, orderer);
}
static MenuItem Join(IEnumerable<MenuItem> items) {
if (items.Count() < 2)
return items.Single();
var joined = new MenuItem {
Text = items.First().Text,
RouteValues = items.First().RouteValues,
Contained = Merge(items.Select(x => x.Contained)).ToArray(),
Position = SelectBestPositionValue(items.Select(x => x.Position))
};
return joined;
}
private static string SelectBestPositionValue(IEnumerable<string> positions) {
var comparer = new PositionComparer();
return positions.Aggregate(string.Empty,
(agg, pos) =>
string.IsNullOrEmpty(agg)
? pos
: string.IsNullOrEmpty(pos)
? agg
: comparer.Compare(agg, pos) < 0 ? agg : pos);
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
namespace Orchard.UI.Navigation {
public class PositionComparer : IComparer<string> {
public int Compare(string x, string y) {
if (x == null || y == null) {
return x == null && y == null ? 0 : (x == null ? -1 : 1);
}
if (x == "" || y == "") {
return x == "" && y == "" ? 0 : (x == "" ? -1 : 1);
}
var xRange = new Range { Length = x.Length };
var yRange = new Range { Length = y.Length };
while (xRange.Start != xRange.Length || yRange.Start != yRange.Length) {
var xSize = xRange.NextDot(x);
var ySize = yRange.NextDot(y);
if (xSize == 0 || ySize == 0) {
// one or both sides are empty
if (xSize != 0 || ySize != 0) {
// favor the side that's not empty
return xSize - ySize;
}
// otherwise continue to the next segment if both are
}
else if (xRange.NumericValue != -1 && yRange.NumericValue != -1) {
// two strictly numeric values
// return the difference
var diff = xRange.NumericValue - yRange.NumericValue;
if (diff != 0)
return diff;
// or continue to next segment
}
else {
if (xRange.NumericValue != -1) {
// left-side only has numeric value, right-side explicitly greater
return -1;
}
if (yRange.NumericValue != -1) {
// right-side only has numeric value, left-side explicitly greater
return 1;
}
// two strictly non-numeric
var diff = string.Compare(x, xRange.Start, y, yRange.Start, Math.Min(xSize, ySize),
StringComparison.OrdinalIgnoreCase);
if (diff != 0)
return diff;
if (xSize != ySize)
return xSize - ySize;
}
xRange.Advance();
yRange.Advance();
}
return 0;
}
struct Range {
public int Start { get; private set; }
private int End { get; set; }
public int Length { get; set; }
public int NumericValue { get; private set; }
public int NextDot(string value) {
if (Start == -1) {
End = -1;
return 0;
}
End = value.IndexOf('.', Start);
int numeric;
NumericValue = int.TryParse(Segment(value), out numeric) ? numeric : -1;
return End == -1 ? Length - Start : End - Start;
}
private string Segment(string value) {
if (Start == 0 && End == -1) {
return value;
}
if (End == -1) {
return value.Substring(Start);
}
return value.Substring(Start, End - Start);
}
public void Advance() {
if (End == -1)
Start = Length;
else
Start = End + 1;
}
}
}
}