
在WebForms中,大家应该都体会过SiteMapPath给开发带来的便利,而今格式各样的导航栏、导航菜单已经成了网站不可缺少的一部分,接下去大家会看到一个在MVC下使用的,并且符合MVC设计规范的导航栏“插件”,以在MVC中取代之前SiteMapPath的应用。

在WebForms中,大家应该都体会过SiteMapPath给开发带来的便利,而今格式各样的导航栏、导航菜单已经成了网站不可缺少的一部分,接下去大家会看到一个在MVC下使用的,并且符合MVC设计规范的导航栏“插件”,以在MVC中取代之前SiteMapPath的应用。
首先我们还是明确一下这个插件的意义和需要完成的基本功能:
问:既然有SiteMapPath,为什么还要重复开发一个同样功能的导航栏?
答:没错,SiteMapPath服务器控件在MVC(以下无特别说明都专指ASP.NET MVC)中仍然可以很好地“显示”,但是显然无法很好满足C-V结构的分离,SiteMapPath控件依赖于aspx页面,而在MVC中,早在aspx页面执行之前,几乎所有数据都应该在Controller处理完成,“打包”给ViewData。这就要求这个控件能够同时在Controller和View中被很好地控制,并且View主要只起到显示的作用。还有一点就是SiteMapPath默认的sitemap格式已经无法满足MVC中http请求的规则,使得无法很好地进行控制。
问:新开发的导航栏有哪些功能?
答:
1、完全兼容原有WebForms项目下的Web.sitemap文件格式,即当网站中同时存在MVC和WebForms项目时, 可以共享sitemap文件。但须按照MVC的执行方式对原有文件稍加补充。
2、自动从Web.sitemap获得当前页面(Controller,Action)对应的网站地图位置,自动生成导航条, 使用时不需要编写任何代码。
3、根据MVC的Controller-Action规则自动创建对应链接,也可自由设置,包括只显示文字,不使用连接等。
4、可以完全或部分手动设置、增减节点。
5、可以限制节点显示层数。
以上的大部分功能都是在SiteMapPath可实现的,但是我们已经不再需要PostBack功能的设置。
遵照这些前提,我给大家展示一下我的实现方法:
一、建立全局共享的Model层的NavigationInfo,包含在Models/ExtentionEntity.cs中
public class NavigationInfo


{


public string Title
{ set; get; }


public string ActionName
{ set; get; }


public string ControllerName
{ set; get; }


public object Values
{ set; get; }

public void SetNavInfo(string title, string actionName, string controllerName, object values)


{

Title = title;

ActionName = actionName;

ControllerName = controllerName;

Values = values;

}

public NavigationInfo()


{ }

public NavigationInfo(string title, string actionName, string controllerName, object values)


{

this.SetNavInfo(title, actionName, controllerName, values);

}

public NavigationInfo(string title, string actionName, string controllerName)


{

this.SetNavInfo(title, actionName, controllerName, null);

}

public NavigationInfo(string title, string actionName, object values)


{

this.SetNavInfo(title, actionName, null, values);

}

public NavigationInfo(string title)


{

this.SetNavInfo(title, null, null, null);

}

}


其中,Title、ActionName、ControllerName、Values分别对应View页面ActionLink需要的链接文字、action、controller和values。里面也提供了对NavigationInfo的4种重写的方法,以便和ActionLink的参数重写尽量配套,在适应使用习惯的同时也提供了更大的灵活性。
二、创建BaseViewData,所有继承了这个类的ViewData都将获得NavigationInfo属性。Models/BaseViewData.cs
public class BaseViewData


{


public string Title
{ get; set; }//这个Title现在这儿埋个伏笔,我会在下文中说明,和导航拦没有太直接关系


public List<ExtentionEntity.NavigationInfo> NavInfo
{ get; set; }

}

三、创建Views/Shared/Navigation.ascx,供页面调用。
注:这里我使用了用户控件的形式是因为考虑到开发时实际调用导航栏的页面不会太多(一般都只在母板页),如果你觉引用的比较多,这样不太方便,也可以加到HtmlHelper中,其中要执行的代码是一样的:

<%
@ Control Language="C#" AutoEventWireup="true" CodeBehind="Navigation.ascx.cs"

Inherits="MVCTools.Views.Shared.Navigation" %>

<div id="Navigation" class="Navigation">


<%

if (ViewData.ContainsDataItem("navInfo")) {

System.Collections.Generic.List<MVCTools.Models.ExtentionEntity.NavigationInfo> navInfo = new System.Collections.Generic.List<MVCTools.Models.ExtentionEntity.NavigationInfo>();

if (ViewData["navInfo"] == null || ((System.Collections.Generic.List<MVCTools.Models.ExtentionEntity.NavigationInfo>)ViewData["navInfo"]).Count == 0)

{

navInfo = MVCTools.Common.Navigation.GetAutoNavigationInfo();

}

else

{

navInfo = (System.Collections.Generic.List<MVCTools.Models.ExtentionEntity.NavigationInfo>)ViewData["navInfo"];

}


%>您的位置:<%

int i = 0;

foreach (MVCTools.Models.ExtentionEntity.NavigationInfo nav in navInfo)

{

if (++i > 1)


{%> ><%
--//TODO:间隔标记可以扩展为用户自定义--%> <%}%>


<%
if (nav.Values != null)

{

if (!string.IsNullOrEmpty(nav.ActionName))//if (nav.Values.ToAttributeList().Contains("controller"))


{%><%= Html.ActionLink(nav.Title,nav.ActionName, nav.Values)%><%
}

else


{%><%= nav.Title%><%
}

}

else if (!string.IsNullOrEmpty(nav.ActionName))


{%><%= Html.ActionLink(nav.Title, nav.ActionName, nav.ControllerName)%><%
}

else


{ %><%= nav.Title%><%
}

} %>


<%
} %>

</div>


在aspx中,我们只需要这样引用就行了:
<!-- 导航条 -->

<%= Html.RenderUserControl("/Views/Shared/Navigation.ascx")%>

四、准备工作基本做好了,下面来看一下在Controller中如何对导航栏灵活控制。
在此之前,我需要在Common中建了一个专门负责处理NavigationInfo的类Common/Navigation.cs:

Code
1
public static class Navigation
2
3
{
4
5
/**//// <summary>
6
7
/// 自动获取所有节点
8
9
/// </summary>
10
11
/// <returns></returns>
12
13
public static List<ExtentionEntity.NavigationInfo> GetAutoNavigationInfo()
14
15
{
16
17
return GetAutoNavigationInfo(999);//有多少层都取下来
18
19
}
20
21
/**//// <summary>
22
23
/// 指定获取layer层节点
24
25
/// </summary>
26
27
/// <param name="layer">从1层开始计,非0。这么做是为了区别List数据操作和对Nav数据操作</param>
28
29
/// <returns></returns>
30
31
public static List<ExtentionEntity.NavigationInfo> GetAutoNavigationInfo(int layer)
32
33
{
34
35
List<ExtentionEntity.NavigationInfo> navInfo = new List<ExtentionEntity.NavigationInfo>();
36
37
//获取sitemap文件,TODO:如果有多个文件,这里可以改成对Web.config中SiteMapProvider设置的引用
38
39
XElement sitemapX = XElement.Load(HttpContext.Current.Server.MapPath("~/Web.sitemap"));
40
41
IEnumerable<XElement> sitemapNodes = sitemapX.Elements();
42
43
string action = "Index";//默认action为"Index"
44
45
string controller = "";
46
47
string pageUrl = System.Web.HttpContext.Current.Request.Url.PathAndQuery;
48
49
bool goNext = false;
50
51
int theLayer = 0;//层计数
52
53
do
54
55
{
56
57
goNext = false;
58
59
foreach (XElement node in sitemapNodes)
60
61
{
62
63
//string nodeUrl = node.Attribute("url").Value;
64
65
string sitemapUrl = ResolveUrl(node.Attribute("url").Value);
66
67
if (CheckUrlMatches(sitemapUrl, pageUrl))
68
69
{
70
71
if (node.Attributes().Count(a => a.Name == "controller") > 0)
72
73
{
74
75
controller = node.Attribute("controller").Value;//添加controller信息
76
77
}
78
79
if (node.Attributes().Count(a => a.Name == "action") > 0)
80
81
{
82
83
action = node.Attribute("action").Value;//添加action信息
84
85
}
86
87
string title = node.Attribute("title").Value;//获取title信息
88
89
navInfo.Add(new ExtentionEntity.NavigationInfo(title, action, new
{ controller = controller }));
90
91
sitemapNodes = sitemapNodes.Elements();
92
93
goNext = true;
94
95
break;
96
97
}
98
99
}
100
101
} while (goNext && ++theLayer < layer);
102
103
return navInfo;
104
105
}
106
107
/**//// <summary>
108
109
/// 插入导航条记录。
110
111
/// </summary>
112
113
/// <param name="navInfos">原始导航数据列表</param>
114
115
/// <param name="navInfo">需要插入的节点数据</param>
116
117
/// <param name="layer">
118
119
/// layer大于0:从根节点开始计,插入到该层之前(和原始Insert方法相同,但是从1开始记)。
120
121
/// layer小于0:从最后一层开始记,插入到该层之前。
122
123
/// layer等于0:插入到最后(和原始Add方法等效)。
124
125
/// 如果越界,则自动调整为可以取到的最近阀值
126
127
/// </param>
128
129
public static void Insert(this List<ExtentionEntity.NavigationInfo> navInfos, ExtentionEntity.NavigationInfo navInfo, int layer)
130
131
{
132
133
if (layer > 0)
134
135
{
136
137
if (layer <= navInfos.Count)
138
139
navInfos.Insert(layer - 1, navInfo);
140
141
else//上标越界
142
143
navInfos.Insert(navInfos.Count, navInfo);//纠正为追加到末尾
144
145
}
146
147
else if (layer < 0)
148
149
{
150
151
if (Math.Abs(layer) >= navInfos.Count)
152
153
navInfos.Insert(navInfos.Count - Math.Abs(layer), navInfo);
154
155
else//下标越界
156
157
navInfos.Insert(0, navInfo);//纠正为插入到第一条
158
159
}
160
161
else//layer = 0
162
163
{
164
165
navInfos.Add(navInfo);
166
167
}
168
169
}
170
171
public static bool CheckUrlMatches(string sitemapUrl, string pageUrl)
172
173
{
174
175
if (!sitemapUrl.EndsWith("/"))
176
177
{
178
179
sitemapUrl += "/";
180
181
}
182
183
if (!pageUrl.EndsWith("/"))
184
185
{
186
187
pageUrl += "/";
188
189
}
190
191
if (Regex.Matches(pageUrl, sitemapUrl + @""w*", RegexOptions.IgnoreCase).Count > 0)
192
193
{
194
195
return true;
196
197
}
198
199
else
200
201
{
202
203
return false;
204
205
}
206
207
}
208
209
/**//// <summary>
210
211
/// Creates a client-resolvable Url based on the passed-in value
212
213
/// </summary>
214
215
/// <param name="virtualUrl">Relative Url to evaluate. Use ~/ to resolve from the root</param>
216
217
/// <returns></returns>
218
219
public static string ResolveUrl(string virtualUrl)
220
221
{
222
223
string result = virtualUrl;
224
225
if (virtualUrl.StartsWith("~/"))
226
227
{
228
229
virtualUrl = virtualUrl.Remove(0, 2);
230
231
//get the site root
232
233
string siteRoot = HttpContext.Current.Request.ApplicationPath;// ctx.Request.ApplicationPath;
234
235
if (siteRoot == string.Empty)
236
237
siteRoot = "/";
238
239
result = siteRoot + virtualUrl;
240
241
}
242
243
return result;
244
245
}
246
247
}
248
249
上面可以看到,相比使用XMLDocument的传统方式,Linq to XML大大简化了对XML的操作。其中关键的地方都作了比较详细的注释,欢迎大家提出建议或批评!
下面是上述方法在Controller中的使用。
注:这里还有一个注意点:虽然NavigationInfo中的Title提供直接在ActionLink中显示的text内容,但是建议不要在Controller中定义的过死,否则可能导致C-V数据的脱节,我在Controller演示设置NavigationInfo的功能,并不在于要在Controller中就把导航栏HTML代码输出,只是输出一个包含节点信息的List给ViewDate,让其在View中具体生成。
我举NavController为例,里面有一个Action叫void More(string type):
public void More(string type)


{

Models.MoreViewData vd = new MVCTools.Models.MoreViewData();

switch (type)


{

case "manual"://完全手动创建

vd.SomePageInfo = "完全手动创建";

vd.NavInfo = new List<MVCTools.Models.ExtentionEntity.NavigationInfo>()


{

new ExtentionEntity.NavigationInfo("手动创建第一节","Index","Home"),

new ExtentionEntity.NavigationInfo("手动创建第二节","Index","Home"),

new ExtentionEntity.NavigationInfo("手动创建第三节","Index","Home"),

new ExtentionEntity.NavigationInfo("手动创建第四节","Index","Home"),

//只设Title时,此节点不会显示为链接

new ExtentionEntity.NavigationInfo("手动创建第五节"),

};

//最后插入“根节点”,使用扩展方法

vd.NavInfo.Insert(new ExtentionEntity.NavigationInfo("手动创建的根节点", "Index", "Home"), 1);

//以下方法和上一句等效

//vd.NavInfo.Insert(0,new ExtentionEntity.NavigationInfo("手动创建的根节点", "Index", "Home"));

break;

case "add"://增加节点

vd.SomePageInfo = "自动获取基础上增加节点";

vd.NavInfo = Navigation.GetAutoNavigationInfo();//自动获取全部节点

//在前面插入

vd.NavInfo.Insert( new ExtentionEntity.NavigationInfo("手动创建第一个"),-1);

//在后面插入

vd.NavInfo.Add(new ExtentionEntity.NavigationInfo("手动创建第二个"));

vd.NavInfo.Add(new ExtentionEntity.NavigationInfo("手动创建第三个"));

//以下方法和上一句等效

//vd.NavInfo.Insert(new ExtentionEntity.NavigationInfo("手动创建第三个"), 0);

break;

case "subtract"://限制、扣除节点

vd.SomePageInfo = "自动获取基础上限制节点,只显示到第2层";

vd.NavInfo = Navigation.GetAutoNavigationInfo(2);

break;

default:

//自动获取,什么都不用做

vd.SomePageInfo = "完全自动读取Web.sitemap";

break;

}

RenderView("More",vd);

}


通过上面case的4种情况我向大家演示了文章开头我们需要完成的5项基本功能,其中如果你不需要对导航栏信息修改,让其自动获取的话,只要传递一个BaseViewData给页面,什么都不用做:
public void More()

{
MoreViewData vd = new MoreViewData ();
//Your Code
RenderView("More ", vd);
}
而接下去在aspx页面(Views)中,你可以什么都不要做,如果需要获取信息或者修改的话只需要查看ViewData.NavInfo或者ViewData[“NavInfo”]就行了。
这里要小说一下上面提到的BaseViewData的Title参数,是用于网页标题Title的显示,由于Title基本属于View层面的东西,在这里设Title主要是方便Controller和View的沟通,具体设置也可以在View中完成(我还是觉得在程序员和美工有很好沟通的情况下,在Controller先设置好基础部分更方便一些,而且不用劳烦每个aspx文件都对title设置,那样有时实在很辛苦)。这个Title你只需要设置最“个性化”的信息,全局的包括每个母板的特定信息会自动加上。有了这个功能,你还可以自己进行一些扩展,比如:把导航栏的最后一个节点的信息作为标题等等。由于和导航栏没有非常直接的关系,这里我暂不多加论述,具体应用你可以在我的Demo中的母板页文件看到(注意.Master.cs文件)
这里是上面代码的Demo下载:/Files/szw/MVCTools_mvc.rar
补充一下,对于sitemap的变化,只是为每个节点多加了两个controller和action属性,以控制节点显示的链接,具体大家可以看Demo中的Web.sitemap
这个导航栏的实现很简单,希望能够发上来抛砖引玉,也省去一些朋友重复劳动之苦。如果有任何不周之处,还望大家见谅和赐教!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异