ASP.NET CORE MVC动态生成左侧菜单栏

ASP.NET CORE MVC动态生成左侧菜单栏

因为自己的一个小项目中需要动态的去构建任务栏,上网查询得到了以下的处理方式:

①利用数据库中的两张表去动态生成

http://t.zoukankan.com/insus-p-5416003.html

既然它是利用数据库来动态的生成任务栏,那么我们这里可不可以分步骤来进行拆分:
①利用一段代码把我们本地存放文件的双层文件夹内数据导入数据库的父子两站表中
②再利用网上的利用数据库来动态的生成任务栏去动态的生成两级目录。

自己的处理:

虽然这种方式在想法上是可行的,但是过程个人觉得有点复杂了,所以打算先上网再搜索下是否还有其他较为简单的解决方法。
附上参考网页上主要的几张图:
ASP.NET CORE MVC动态生成左侧菜单栏_第1张图片
ASP.NET CORE MVC动态生成左侧菜单栏_第2张图片

ASP.NET CORE MVC动态生成左侧菜单栏_第3张图片

②利用ViewComponent来实现动态导航菜单

外网上找到了一个比较合适的框架,并找到其源代码:https://github.com/IntrinsicInnovation/CoreMenuBuilder

日志2022年11月14日

这里的CoreMenuBuilder中视图组件运用的过程中,并没有涉及到MVC中的控制器对视图的映射

所以对于我们这里的项目中动态任务栏模块的话就可以直接按照CoreMenuBuilder中的对应去进行修改相应的内容即可

ASP.NET Core MVC之ViewComponents(视图组件)

具体的参考微软官网:https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-7.0&viewFallbackFrom=aspnetcore-2.1

​ 视图组件类似于我们之前ASP.NET MVC中的部分视图,不过其功能比部分视图更加强大,它不会依赖于强类型视图,也和部分视图一样重在重用,到底多强大我们下面一起来见识下。首先我们过一过基本原理。

视图组件:

  • 呈现一个区块而不是整个响应
  • 包括控制器和视图间发现的相同关注点分离和可测试性优势。
  • 可以有参数和业务逻辑。
  • 通常从布局页调用。

视图组件可用于具有可重用呈现逻辑(对分部视图来说过于复杂)的任何位置,例如:

  • 动态导航菜单
  • 标记云(查询数据库的位置)
  • 登录面板
  • 购物车
  • 最近发布的文章
  • 典型博客上的边栏内容
  • 一个登录面板,呈现在每页上并显示注销或登录链接,具体取决于用户的登录状态
创建视图组件:

可通过以下任一方法创建视图组件类:

  • 从 ViewComponent 派生
  • 使用 [ViewComponent] 属性修饰类,或者从具有 [ViewComponent] 属性的类派生
  • 创建名称以 ViewComponent 后缀结尾的类

与控制器一样,视图组件必须是公共、非嵌套和非抽象的类。 视图组件名称是删除了“ViewComponent”后缀的类名。 也可以使用 ViewComponentAttribute.Name 属性显式指定它。

视图组件方法

视图组件以返回 TaskInvokeAsync 方法,或是以返回 IViewComponentResult 的同步 Invoke 方法定义其逻辑。 参数直接来自视图组件的调用,而不是来自模型绑定。 视图组件从不直接处理请求。 通常,视图组件通过调用 View 方法来初始化模型并将其传递到视图。 总之,视图组件方法:

  • 定义返回 TaskInvokeAsync 方法,或是返回 IViewComponentResult 的同步 Invoke 方法。
  • 一般通过调用 ViewComponent View 方法来初始化模型并将其传递到视图。
  • 参数来自调用方法,而不是 HTTP。 没有模型绑定。
  • 不可直接作为 HTTP 终结点进行访问。 通过代码调用它们(通常在视图中)。 视图组件从不处理请求。
  • 在签名上重载,而不是当前 HTTP 请求的任何详细信息。

一般通过调用 InvokeAsync 方法来调用视图组件,此方法定义在 IViewComponentHelper 接口中

视图搜索路径

运行时在以下路径中搜索视图:

  • /Views/{Controller Name}/Components/{View Component Name}/{View Name}
  • /Views/Shared/Components/{View Component Name}/{View Name}
  • /Pages/Shared/Components/{View Component Name}/{View Name}

搜索路径适用于使用控制器 + 视图和 Razor Pages 的项目。

视图组件的默认视图名称为 Default,这意味着视图文件通常命名 Default.cshtml。 可以在创建视图组件结果或调用 View 方法时指定不同的视图名称。

建议命名视图文件 Default.cshtml ,并使用 Views/Shared/Components/{View Component Name}/{View Name} 路径PriorityList此示例Views/Shared/Components/PriorityList/Default.cshtml中使用的视图组件用于视图组件视图。




实际运用:

从项目中调用的角度出发的角度来讲:

①在_Layout.cshtml填写入一个视图组件占位符

②然后来到其中的_SideMenu.cshtml模块

<div class="admin-tree-menu">
    <ul id="AdminTree" class="vertical menu accordion-menu" data-accordion-menu>
        @*调用视图组件所在地,通过视图组件渲染出来的动态任务栏也在这里呈现*@
       @* Component.InvokeAsync(我们要调用的组件名称,传递给该视图组件的参数)*@
        @await Component.InvokeAsync("Menu", new { testId = 1 })  @*ViewBag.Id })*@  @*Use ViewBag if you want to pass a value into InvokeAsync dynamically*@
    ul>
div>

Component.InvokeAsync(我们要调用的组件名称,传递给该视图组件的参数)

③在这里实现过程中转而去调用Component.InvokeAsync函数。

 /// 
    /// 该ViewComponent调用其他方法来实际生成菜单。
    /// 利用MenuHelper类对象去进行相关的多级菜单的创建过程(其中调用了ViewModels中的模型类)
    /// 注意:【这里的视图组件名称为Menu】
    ///       ①与控制器一样,视图组件必须是公共、非嵌套和非抽象的类。 视图组件名称是删除了“ViewComponent”后缀的类名。 也可以使用 ViewComponentAttribute.Name 属性显式指定它。
    ///       ②views/shared下的视图组件的文件夹:文件夹名称必须和view component 类名称一致(如果使用了ViewComponent属性)
    /// 
    public class MenuViewComponent : ViewComponent
    {
        private readonly MenuHelper _cbaMenuHelper = new MenuHelper();

        /// 
        /// 返回菜单树结构(即为这里的view)。然后由调用ViewComponent的Razor页面显示
        /// 
        /// 
        /// 返回
        public async Task InvokeAsync(int testId)
        {
            //testId is optional, and can be passed in when you call InvokeAsync
            //Parse current URL to determine which menu item to highlight:
            string baseUrl = Request.Scheme + "://" + Request.Host.Value;   //https://localhost:5001
            var httpRequestFeature = Request.HttpContext.Features.Get();
            var uri = httpRequestFeature.RawTarget; // /
            var id = HttpContext.Request.Query["id"].ToString();  //Parse Query String
            var menuItems = await _cbaMenuHelper.GetAllMenuItems(baseUrl + uri, id); //Can pass in testId here if required. (生成任务栏数据的地方)
             //第一个参数为视图名称,如果没有,即为:“Default”,第二个参数为传到该分部视图的参数
            return View("_MenuPartial", _cbaMenuHelper.GetMenu(menuItems, null)); 
        }

    }

④在这个视图组件类的InvokeAsync函数调用的过程中,会用到MenuHelper.cs中的GetAllMenuItemsGetMenu方法。最后会返回一个视图,而这个视图正是我们的生成任务栏所在。

GetAllMenuItems通过这个函数,导入我们的目录文件并由此去在MenuViewModel中去对子菜单Menu类对象进行数据填充

GetMenu通过这个函数对一级目录下的子文件进行填充进而去MenuViewModel中去对父菜单MenuViewModel类对象进行数据填充。

_MenuPartial :通过_MenuPartial的结构和_cbaMenuHelper.GetMenu(menuItems, null)得到的菜单类对象数据,我们就可以对任务栏的标签属性和值进行对应动态生成。_MenuPartial的结构如下:

@model System.Collections.Generic.IEnumerable<OnlineAnswerSystem_1.ViewModels.MenuViewModel>

@*一旦你获得菜单项的“分层”列表menuItems后,只需将其返回到InvokeAsync()方法内显示的视图中即可。在这种情况下,视图只是根据菜单记录中的父子关系递归显示菜单项:*@

@foreach (var menu in Model)
{
    if (menu.Children.Any())
    {
        <li class="cm-submenu">
            <a href="#" class="sf-window-layout @menu.Content">@menu.Content<span class="caret">span>a>
            <ul class="menu vertical nested is-active">
                <partial name="Components/Menu/_MenuPartial" model="menu.Children" />
            ul>
        li>
        <script>
            document.getElementsByClassName(@menu.Content).setAttribute('style', 'background-color: red!important;');
        script>
    }
    else
    {
          <li class="@menu.SelectedClass" style="@menu.SelectedClass">
            @{
                if (string.IsNullOrEmpty(menu.Url))   //Add either onclick event or href.
                {
                                                <a onclick="@menu.OnClick">
                                                    <i class="@menu.IconClass">i><span style="@menu.SelectedStyle">@menu.Contentspan>
                                                a>
                }
                else
                {
                                                <a href="@menu.Url"  style="background:none;">

                                                    @menu.Content
                                                a>
                }
            }
        li>
    }
}


从调用的角度出发的话:

ViewComponents不必依赖已存在的数据。例如,您可以简单地对服务器端方法进行异步调用,如下所示:

@await Component.InvokeAsync("MenuItems", 1234)

具体内容如下:
这是View Component本身:

  public class MenuViewComponent : ViewComponent
    {
        private readonly MenuHelper _cbaMenuHelper = new MenuHelper();

        public async Task<IViewComponentResult> InvokeAsync(int testId)  //int testId
        {
            //testId is optional, and can be passed in when you call InvokeAsync
            //Parse current URL to determine which menu item to highlight:
            string baseUrl = Request.Scheme + "://" + Request.Host.Value;   //https://localhost:5001
            var httpRequestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
            var uri = httpRequestFeature.RawTarget; // /
            //var id = HttpContext.Request.Query["id"].ToString();  //Parse Query String
            var menuItems = await _cbaMenuHelper.GetAllMenuItems(baseUrl + uri, testId); //Can pass in testId here if required. (生成任务栏数据的地方)
            return View("_MenuPartial", _cbaMenuHelper.GetMenu(menuItems, null)); //
        }
    }

该****ViewComponent****调用其他方法来实际生成菜单。在这种情况下,它只是一个Parent - >child映射菜单对象列表,以下代码在此供您参考:

  public class Menu  //子菜单类
    {
        public Guid ID { get; set; }
        public Guid? ParentID { get; set; }
        public string Content { get; set; }
        public string IconClass { get; set; }
        public string Url { get; set; }
        public string SelectedStyle { get; set; }  // If you want a style vs a class;
        public string SelectedClass { get; set; }
        public string OnClick { get; set; }
        public long Order { get; set; }
    }
    public class MenuViewModel  //父菜单类
    {
        public Guid ID { get; set; }
        public string Content { get; set; }
        public string IconClass { get; set; }  //图标
        public string Url { get; set; }
        public string SelectedStyle { get; set; }  //当前选中的样式
        public string SelectedClass { get; set; }
        public string OnClick { get; set; }
        public IList<MenuViewModel> Children { get; set; }  //子菜单
    }

一旦你获得菜单项的“分层”列表后,只需将其返回到InvokeAsync()方法内显示的视图中即可。在这种情况下,视图只是根据菜单记录中的父子关系递归显示菜单项:

@model System.Collections.Generic.IEnumerable<OnlineAnswerSystem_1.ViewModels.MenuViewModel>

@*一旦你获得菜单项的“分层”列表menuItems后,只需将其返回到InvokeAsync()方法内显示的视图中即可。在这种情况下,视图只是根据菜单记录中的父子关系递归显示菜单项:*@

@foreach (var menu in Model)
{
    if (menu.Children.Any())
    {
        <li class="cm-submenu">
            <a href="#" class="sf-window-layout @menu.Content">@menu.Content<span class="caret">span>a>
            <ul class="menu vertical nested is-active">
                <partial name="Components/Menu/_MenuPartial" model="menu.Children" />
            ul>
        li>
        <script>
            document.getElementsByClassName(@menu.Content).setAttribute('style', 'background-color: red!important;');
        script>
    }
    else
    {
          <li class="@menu.SelectedClass" style="@menu.SelectedClass">
            @{
                if (string.IsNullOrEmpty(menu.Url))   //Add either onclick event or href.
                {
                                                <a onclick="@menu.OnClick">
                                                    <i class="@menu.IconClass">i><span style="@menu.SelectedStyle">@menu.Contentspan>
                                                a>
                }
                else
                {
                                                <a href="@menu.Url"  style="background:none;">

                                                    @menu.Content
                                                a>
                }
            }
        li>
    }
}


这将返回包装在一个实例中IViewComponentResult,该实例是从****ViewComponent*返回的受支持的结果类型之一*。

这是从布局页面调用ViewComponent的调用,如下所示:

@await Component.InvokeAsync("Menu", new { cbaId = ViewBag.Id })

在这种情况下我使用了ViewBag将ID传递给ViewComponent,因此有一些显示内容的上下文。您还可以在ViewComponent中查看我检索当前Route,并使用它以及上面的Id来确定要加载的菜单项。

最终结果如下:

您可以看到排序是正确的,并且使用了Font Awesome图标,一个漂亮的可折叠菜单就这样创建了。单击“新建”按钮可以运行自定义Javascript,最有可能创建一些内容:

其余的项是路径Url,都是从MenuHelper类创建的。我使用基本的ASP.NET Core应用程序作为起点并围绕它“包装”我的菜单组件。

文章中的源代码可在以下地址中找到:https://github.com/IntrinsicInnovation/CoreMenuBuilder

我很快就创建了整个动态菜单系统。并且,它基本上独立于系统中的其他视图和部分视图。只需将我的所有代码剪切并粘贴到您的应用程序中,即可重复使用。这是一个现实世界的例子,解决了现代应用程序开发的非常现实的问题。拥有这样的独立易于测试的模块可确保您的应用程序不太可能在生产环境中引发问题。

过程出现的问题:如何告知用户现在浏览所在的路径

想了觉得有两种方式实现:
①就是任务栏高亮设置
②就是在页面上显式的列出当前的路径

①就是任务栏高亮设置

主要参考文档:

(1)https://blog.csdn.net/WuLex/article/details/122078862

(2)https://blog.csdn.net/weixin_42331508/article/details/108340029?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6-108340029-blog-57516618.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6-108340029-blog-57516618.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=7

(3)https://blog.csdn.net/H_King_H/article/details/90445637?spm=1001.2101.3001.6650.5&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-5-90445637-blog-57516618.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-5-90445637-blog-57516618.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=6

(4)https://learn.microsoft.com/zh-cn/aspnet/core/mvc/views/partial?view=aspnetcore-2.2

第一种方法主要利用的是在MVC结构的页面中,我们在点击页面跳时,我们获取请求路径URL中的controller的值,并利用一个中间变量routetype做出一个判断,如果自己当前所在页面的controller和页面传过来的请求的controller的值一致的话,即将这个目录的SelectedClass属性做设置(eg:background-color:red;width:auto;),可在随后的页面渲染中着重突出,即可实现任务栏高亮。

但是这个方法在我自己的项目中暂时实现不了:

​ 因为我项目中的页面跳转其实在本质上并没有用到MVC架构,利用的都是wwwroot下的url路径进行的页面之间的跳转,所以在页面跳转都处于Home控制器下,所以无法利用控制器做特意性条件。

(ps:我也想过是否可以在program.cs中自己创建一个自定义路由规则,使其跳转过程中其controller值不在是home,但是一想他们本质上是一样的,自定义路由无非就是换了一个controller,但是我还是无法实现动态的controller值)

//创建自定义路由
app.MapControllerRoute(
     name: "Blog",  // Route name
    pattern: "Archive/{entryDate}",   // URL with parameters
    new { controller = "Archive", action = "Entry" }   // Parameter defaults
    );

还是就是在这种方式中之前也想过用js来实现,但是也失败了。(参考:(33条消息) CSS 导航栏选中框背景高亮显示_A1690363967的博客-CSDN博客_css 背景高亮)

设置样式:

<style type="text/css">
        .menuBackgroundColor{
            background-color:red !important;
        }
        #cm-menu-scroller #AdminTree li a.sf-window-layout {
            background-color:white;
        }
    style>

js代码:

 <script type="text/javascript">
       //获取div下面所有的a标签(返回节点对象)
       var myNav = document.getElementById("menu").getElementsByTagName("a");

       //获取当前窗口的url
       var myURL = document.location.href;
      print(myURL);
       //循环div下面所有的链接,
       for(var i=1;i<myNav.length;i++){
       //获取每一个a标签的herf属性
      var links = myNav[i].getAttribute("href");
      var myURL = document.location.href;

      //查看div下的链接是否包含当前窗口,如果存在,则给其添加样式
      if(myURL.indexOf(links) != -1){
        myNav[i].className="active";
        myNav[0].className="";
       }
 }
    script>
②就是在页面上显式的列出当前的路径

​ 这种方式就是打算在js上下手去获取页面跳转时的页面请求并从中获取出我们需要的路径值(这里可以实现是因为我们前面设置目录下文件的url的时候就是按照其在windows系统的文件路径来动态生成的)。

<header id="cm-header">
       <nav class="cm-navbar cm-navbar-primary">
           <div class="btn btn-primary md-menu-white hidden-md hidden-lg" data-toggle="cm-menu">div>
           <div class="cm-flex">
               @*<h1>@ViewData["Title"]h1>*@
               <div id="url">div>
               <script type="text/javascript">
                   var box = document.getElementById("url")
                   var h1 = document.createElement("h1")
                   var surl = decodeURI(window.location.pathname); //对含有中文的url进行解码(但不同浏览器的编码方式不同,这里可能后面需要更改)
                   if (surl.split("posts")[1]==undefined){  //2022年11月17日:强行处理首页中的小bug(首页路径显示处显示undefined)
                       h1.innerText = "/Home/Index";
                   }else{
                       h1.innerText = surl.split("posts")[1];  /*这里到时候肯定需要进行更改*/
                   } 
                   box.appendChild(h1)
                   script>

​ 考虑到在MVC中页面布局的渲染过程,我们选择在_Layout.cshtml中添加这段js代码(我们程序中的任何页面的渲染过程最先都要经过_Layout.cshtml的渲染)。

ASP.NET CORE MVC动态生成左侧菜单栏_第4张图片

具体的MVC中页面布局的渲染过程可见我的另外一篇博文:https://blog.csdn.net/mwcxz/article/details/127775127?spm=1001.2014.3001.5502


补充一句就是:其实在这之前是试过另外好几种方式,但是都没有成功,这些方法在逻辑上都行得通,具体的失败原因暂时还不清楚。现在把这些出错的方法罗列出来以待后面再看。

①第一种方式:通过C#来实现获取当面页面中的URL

但是总是因为函数使用不了而出错。感觉网上经常用的那几个都不知道.net6

string url = Request.Url.AbsoluteUri; //结果: http://www.jbxue.com/web/index.aspx 
string host = Request.Url.Host; //结果:www.jbxue.com
 string rawUrl = Request.RawUrl; //结果:/web/index.aspx 
string localPath = Request.Url.LocalPath; //结果:/web/index.aspx 

HttpContext.Current.Request也是Request(都出错)

ASP.NET CORE MVC动态生成左侧菜单栏_第5张图片

②第二种方式:通过JS来实现获取当面页面中的URL

<script type="text/javascript">
        document.getElementsByTagName("h1")[0].Value= window.location.pathname; //
    script>

可以获取当前页面的url值,同时也可以改变这里的h1标签的值,但是渲染出来的页面就是不显示它的值
在这里插入图片描述

可能的原因:
第一个就是这里的js代码渲染慢了**(不成立,这里h1的值都变了,证明还是可以修改h1的属性的)**

第二个就是

value

这里的value的赋值到底要通过那个方法来实现。

属性用错了:应该是用innerHtml document.getElementsByTagName(“h1”)[0].innerHtml = window.location.pathname;

参考:(33条消息) javaScript操作在html中写入标签和内容_GuessHat的博客-CSDN博客

你可能感兴趣的:(学习,.net,c#,mvc,asp.net,数据库)