ارتقاء به ASP.NET Core 1.0 - قسمت 13 - معرفی View Components
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

روش رندر یک View در ASP.NET MVC، بر مبنای اطلاعاتی است که از کنترلر، در اختیار View آن قرار می‌گیرد. اما گاهی از اوقات نیاز است بعضی از قسمت‌های صفحه همواره نمایش داده شوند (مانند نمایش تعداد کاربران آنلاین، سخن روز، منوهای کنار صفحه و امثال آن). یک راه حل برای این مساله، اضافه کردن اطلاعات مورد نیاز View در ViewModel ارائه شده‌ی توسط کنترلر است. هرچند این روش کار می‌کند اما پس از مدتی به ViewModel هایی خواهیم رسید که تشکیل شده‌اند از چندین و چند خاصیت اضافی که الزاما مرتبط با تعریف آن ViewModel نیستند. راه حل بهتر، قرار دادن قسمت‌های مشترک صفحات در فایل layout برنامه است؛ اما فایل layout، به سادگی نمی‌تواند از دایرکتیو model@ برای مشخص سازی مدل و یا مدل‌های مورد نیاز خود استفاده کند (هر چند ممکن است؛ اما بیش از اندازه پیچیده خواهد شد).
در نگارش‌های پیشین ASP.NET MVC، یک چنین مسائلی را با معرفی Child Actionها
    public partial class SidebarMenuController : Controller
    {
        const int Min15 = 900;

        [ChildActionOnly]
        [OutputCache(Duration = Min15)]
        public virtual ActionResult Index()
        {
            return PartialView("_SidebarMenu");
        }
    }
و سپس نمایش آن‌ها توسط Html.RenderAction در فایل layout برنامه، حل می‌کنند. در ASP.NET Core، جایگزین Child Actionها، مفهوم جدیدی است به نام View Components.


یک مثال: تهیه‌ی اولین View Component

ساختار یک View Component، بسیار شبیه است به ساختار یک Controller، اما با عملکردی محدود. به همین جهت کار تعریف آن با افزودن یک کلاس سی‌شارپ شروع می‌شود و این کلاس را می‌توان در پوشه‌ای به نام ViewComponents در ریشه‌ی پروژه قرار داد (اختیاری).


سپس برای نمونه، کلاس ذیل را به این پوشه اضافه کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Core1RtmEmptyTest.Services;
 
namespace Core1RtmEmptyTest.ViewComponents
{
    public class SiteCopyright : ViewComponent
    {
        private readonly IMessagesService _messagesService;
 
        public SiteCopyright(IMessagesService messagesService)
        {
            _messagesService = messagesService;
        }
 
        public IViewComponentResult Invoke(int numberToTake)
        {
            var name = _messagesService.GetSiteName();
            return View(viewName: "Default", model: name);
        }
 
        //public async Task<IViewComponentResult> InvokeAsync(int numberToTake)
        //{
        //    return View();
        //}
    }
}
همانطور که پیشتر نیز عنوان شد، تزریق وابستگی‌ها در تمام قسمت‌های ASP.NET Core در دسترس هستند. در اینجا نیز از سرویس MessagesService بررسی شده‌ی در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها» برای نمایش نام سایت استفاده می‌کنیم.

ساختار کلی یک کلاس ViewComponent شامل دو جزء اصلی است:
الف) از کلاس پایه ViewComponent مشتق می‌شود. به این ترتیب توسط ASP.NET Core قابل شناسایی خواهد شد.
ب) دارای متد Invoke ایی است که بجای Html.RenderAction در نگارش‌های پیشین ASP.NET MVC، قابل فراخوانی است. این متد یک View را باز می‌گرداند.
ج) در اینجا امکان تعریف نمونه‌ی Async متد Invoke نیز وجود دارد (برای مثال جهت کار با متدهای Async بانک اطلاعاتی).
روش فراخوانی این متدها نیز به این صورت است: ابتدا به دنبال نمونه‌ی async می‌گردد. اگر یافت شد، همینجا کار خاتمه می‌یابد. اگر یافت نشد، نمونه‌ی sync یا معمولی آن فراخوانی می‌شود و اگر این هم یافت نشد، یک استثناء صادر خواهد شد.
د) متد Invoke می‌تواند دارای پارامترهای دلخواهی نیز باشد و حالت پیش فرض آن بدون پارامتر است.

روش یافتن یک view component توسط ASP.NET Core به این صورت است:
الف) این کلاس باید عمومی بوده و همچنین abstract نباشد.
ب) «یکی» از مشخصه‌های ذیل را داشته باشد:
1) نامش به ViewComponent ختم شده باشد.
2) از کلاس ViewComponent ارث بری کرده باشد.
3) با ویژگی ViewComponent مزین شده باشد.


نحوه و محل تعریف View یک View Component

پس از تعریف کلاس ViewComponent مورد نظر، اکنون نیاز است View آن‌را اضافه کرد. روش یافتن این Viewها توسط ASP.NET Core نیز بر این مبنا است که
الف) اگر این View Component عمومی و سراسری است، باید درون پوشه‌ی shared، پوشه‌ی جدیدی را به نام Components ایجاد کرده و سپس ذیل این پوشه، بر اساس نام کلاس ViewComponent، یک زیر پوشه‌ی دیگر را ایجاد و داخل آن، View مدنظر را اضافه کرد (تصویر ذیل).
 /Views/Shared/Components/[NameOfComponent]/Default.cshtml
ب) اگر این View Component تنها باید از طریق Viewهای یک کنترلر خاص قابل دسترسی باشند، زیر پوشه‌ی Component یاد شده را ذیل پوشه‌ی View همان کنترلر قرار دهید (و آن‌را از قسمت Shared خارج کنید).
 /Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml


یک نکته: اگر نام کلاسی به ViewComponent  ختم شده بود، نیازی نیست تا ViewComponent  را هم در حین ساخت پوشه‌ی آن ذکر کرد.


نحوه‌ی استفاده‌ی از View Component تعریف شده و ارسال پارامتر به آن

و در آخر برای استفاده‌ی از این View Component تعریف شده، به فایل layout برنامه مراجعه کرده و آن‌را به نحو ذیل فراخوانی کنید:
 <footer>
    <p>@await Component.InvokeAsync("SiteCopyright", new { numberToTake = 5 })</p>
</footer>
اولین پارامتر متد InvokeAsync، همان نام کلاس View Component است. اگر خواستید پارامتر(های) دلخواهی را به متد Invoke کلاس View Component ارسال کنید (مانند پارامتر int numberToTake در مثال فوق)، آن‌را در همینجا می‌توان ذکر کرد (با فرمت dictionary و یا  anonymous type).

یک نکته: متدهای قدیمی Component.Invoke و Component.Renderدر اینجا حذف شده‌اند (اگر مقالات پیش از RTM را مطالعه کردید) و روش توصیه شده‌ی در اینجا، کار با متدهای async است.


تفاوت‌های View Components با Child Actions نگارش‌های پیشین ASP.NET MVC

پارامترهای یک View Component از طریق یک HTTP Request تامین نمی‌شوند و همانطور که ملاحظه کردید در همان زمان فراخوانی آن‌ها به صورت مستقیم فراهم خواهند شد. بنابراین مباحث model binding در اینجا دیگر وجود خارجی ندارند. همچنین View Components جزئی از طول عمر یک کنترلر نیستند. بنابراین اکشن فیلترهای مختلف تعریف شده، تاثیری را بر روی آن‌ها نخواهند داشت (این مشکلی بود که با Child Actions در نگارش‌های قبلی مشاهده می‌شد). همچنین View Components به صورت مستقیم از طریق درخواست‌های HTTP قابل دسترسی نیستند. به علاوه Child actions قدیمی، از فراخوانی‌های async پشتیبانی نمی‌کنند.
زمانیکه کلاسی از کلاس پایه ViewComponent ارث بری می‌کند، تنها به این خواص عمومی از درخواست HTTP جاری دسترسی خواهد داشت:
[ViewComponent]
public abstract class ViewComponent
{
   protected ViewComponent();
   public HttpContext HttpContext { get; }
   public ModelStateDictionary ModelState { get; }
   public HttpRequest Request { get; }
   public RouteData RouteData { get; }
   public IUrlHelper Url { get; set; }
   public IPrincipal User { get; }

   [Dynamic]  
   public dynamic ViewBag { get; }
   [ViewComponentContext]
   public ViewComponentContext ViewComponentContext { get; set; }
   public ViewContext ViewContext { get; }
   public ViewDataDictionary ViewData { get; }
   public ICompositeViewEngine ViewEngine { get; set; }

   //...
}


فراخوانی Ajax ایی یک View Component

در ASP.NET Core، یک اکشن متد می‌تواند خروجی ViewComponent نیز داشته باشد و این تنها روشی است که می‌توان یک View Component را از طریق درخواست‌های HTTP، مستقیما قابل دسترسی کرد:
public IActionResult AddURLTest()
{
   return ViewComponent("AddURL");
}
در این حالت می‌توان این اکشن متد را به صورت Ajax ایی نیز بارگذاری و به صفحه اضافه کرد:
$(document).ready (function(){
    $("#LoadSignIn").click(function(){
         $('#UserControl').load("/Home/AddURLTest");
    });
});


امکان بارگذاری View Components از اسمبلی‌های دیگر نیز وجود دارد

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها» روش دسترسی به Viewهای برنامه را که در اسمبلی آن قرار گرفته بودند، بررسی کردیم. دقیقا همان روش در مورد view components نیز صادق است و کاربرد دارد. جهت یادآوری، این مراحل باید طی شوند:
الف) اسمبلی ثالث حاوی View Component‌های برنامه باید ارجاعاتی را به ASP.NET Core و قابلیت‌های Razor آن داشته باشد:
"dependencies": {
   "NETStandard.Library": "1.6.0",
   "Microsoft.AspNetCore.Mvc": "1.0.0",
   "Microsoft.AspNetCore.Razor.Tools": {
   "version": "1.0.0-preview2-final",
   "type": "build"
  }
},
"tools": {
   "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
}
ب) محل قرارگیری viewهای این اسمبلی ثالث نیز همانند قسمت «نحوه و محل تعریف View یک View Component» مطلب جاری است و تفاوتی نمی‌کند. فقط برای  قرار دادن این Viewها در اسمبلی برنامه باید گزینه‌ی embed را مقدار دهی کرد:
"buildOptions": {
   "embed": "Views/**/*.cshtml"
}
ج) مرحله‌ی آخر هم معرفی این اسمبلی ثالث، به RazorViewEngineOptions به صورت یک EmbeddedFileProvider جدید است. در این مثال، ViewComponentLibrary نام فضای نام این اسمبلی است.
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
   //Get a reference to the assembly that contains the view components
   var assembly = typeof(ViewComponentLibrary.ViewComponents.SimpleViewComponent).GetTypeInfo().Assembly;
   //Create an EmbeddedFileProvider for that assembly
   var embeddedFileProvider = new EmbeddedFileProvider(assembly,"ViewComponentLibrary");
   //Add the file provider to the Razor view engine
   services.Configure<RazorViewEngineOptions>(options =>
   {
      options.FileProviders.Add(embeddedFileProvider);
   });
د) جهت رفع تداخلات احتمالی این اسمبلی با سایر اسمبلی‌ها بهتر است ویژگی ViewComponent را به همراه نامی مشخص ذکر کرد (در حین تعریف کلاس View Component):
 [ViewComponent(Name = "ViewComponentLibrary.Simple")]
public class SimpleViewComponent : ViewComponent
و در آخر فراخوانی این View Component بر اساس این نام صورت خواهد گرفت:
 @await Component.InvokeAsync("ViewComponentLibrary.Simple", new { number = 5 })
  • #
    ‫۷ سال و ۱۰ ماه قبل، جمعه ۱۴ آبان ۱۳۹۵، ساعت ۲۲:۴۵
    با توجه به مطالب قید شده و امکان قرار دادن ویوها در اسمبلی‌های جداگانه آیا همانند مقاله طراحی افزونه پذیر در MVC امکان طراحی سیستمی با همان کارکرد و قابلیتها در asp.net core هم مقدور است؟
    ودر اینصورت سیستمی که با مقاله قبلی طراحی شده چقدر باید دستخوش تغییرات شود تا در asp.net core هم قابل استفاده شود .
  • #
    ‫۷ سال و ۱۰ ماه قبل، یکشنبه ۷ آذر ۱۳۹۵، ساعت ۱۳:۳۳
    ارتقاء به ASP.NET Core 1.1
    روش معرفی پیشین View Components
    @await Component.InvokeAsync("SiteCopyright", new { numberToTake = 5 })
    در مقایسه با Tag Helpers ارائه شده در ASP.NET Core، آنچنان زیبا نیست و با کل مجموعه ناهماهنگ به نظر می‌رسد. به همین جهت در نگارش 1.1، امکان درج و تعریف View Components را به صورت Tag Helpers مهیا کرده‌اند:
    <vc:site-copyright number-to-take="5"></vc:site-copyright>
    که در اینجا تعریف یک ViewComponent با vc شروع می‌شود و سپس نام آن به صورت «کبابی» باید درج شود (Kebab Case)؛ همچنین پارامترهای مرتبط نیز به همین نحو. در روش معرفی «کبابی»، هرجایی که یک حرف، به صورت بزرگ درج شده‌است، یک - قرار می‌گیرد (شبیه به سیخ کباب!).
    همچنین برای فعال سازی :vc نیاز است به فایل ViewImports.cshtml_ مراجعه کرده و اسمبلی جاری را که vc در آن قرار دارد، معرفی کرد:
    @addTagHelper *,Core1RtmEmptyTest
    پس از این تعریف، vcهای اسمبلی معرفی شده، قابلیت تعریف به صورت Tag Helper را خواهند داشت.
  • #
    ‫۷ سال و ۶ ماه قبل، دوشنبه ۷ فروردین ۱۳۹۶، ساعت ۰۰:۲۸
    به روز رسانی
    با حذف فایل project.json در VS 2017، اکنون با کلیک راست بر روی گروه نام پروژه (فایل csproj)، گزینه‌ی Edit آن ظاهر شده و مداخل ذکر شده‌ی در مطلب فوق، چنین تعاریفی را پیدا می‌کنند: 
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <ItemGroup>
        <EmbeddedResource Include="Views\**\*.cshtml"  />
      </ItemGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
      </ItemGroup>
    </Project>
     همچنین Razor Tools به صورت یک افزونه‌ی مجزا درآمده‌است که از اینجا قابل دریافت می‌باشد (و به صورت پیش فرض در VS 2017 نصب است). اطلاعات بیشتر  
  • #
    ‫۶ سال و ۱۱ ماه قبل، شنبه ۲۹ مهر ۱۳۹۶، ساعت ۲۰:۵۷
    در MVC5 با توجه به نیاز پروژه قسمتی از پردازش View به عهده PartialView  بود که با تعریف یک اکشن در همان کنترلر ، این امر محقق می‌شد.
    گاها این پارشیال شامل دیتای خاص ( دریافت از منابع اطلاعاتی خاص) می‌بود که به راحتی با یک درخواست اجکسی به روز می‌شد.
    گویا متد Html.RenderAction در نسخه Core موجود نمی‌باشد و باعث بروز خطای 
    NullReferenceException  می‌شود.
    راهکار ViewComponet از سادگی و کم بودن کدهای مرحله ای که توضیح دادم نیست .
    راه کار مناسب جهت این امر چیست ؟
    • #
      ‫۶ سال و ۱۱ ماه قبل، شنبه ۲۹ مهر ۱۳۹۶، ساعت ۲۲:۳۱
      - همانطور که در مقدمه‌ی بحث هم عنوان شد، مفهوم Child Actions از نگارش Core حذف شده‌است؛ چون مشکلات زیادی دارد (به این لیست، مشکلات async و همچنین آغاز یک چرخه‌ی جدید MVC را هم اضافه کنید که کارآیی مناسبی ندارد و یک سربار به شمار می‌رود).
      - جایگزین آن ViewComponet است و از لحاظ دسترسی به منابع و سرویس‌ها محدودیتی ندارد. یک مثال
      - هنوز هم اگر صرفا نیاز به رندر یک پارشال View را به صورت Ajax ایی دارید، روش زیر کار می‌کند:
      جایی که می‌خواستید Html.RenderAction را قرار دهید، قطعه کد Ajax ایی زیر را فراخوانی کنید:
      <div id="dynamicContentContainer"></div>
      <script>   
          $.get('@Url.Action("GetData", "Home")', {id : 1}, function(content){
                  $("#dynamicContentContainer").html(content);
              });
      </script>
      کار آن دریافت محتوای html ایی اکشن متد ذیل و افزودن آن به div مشخص شده‌است.
      [HttpGet]
      public IActionResult GetData(int id)
      {
         return PartialView(id);
      }
      با این پارشال View فرضی:
      @model int 
      <span>Values from controler :</span> @Model
  • #
    ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۱۸:۱۴
    باسلام و خسته نباشید
    من نیاز به این دارم که تعداد زیادی ViewComponent رو تعریف کنم که در کل پروژه استفاده میشه بنابراین میخوام اونها رو بصورت پوشه‌های تودرتو تعریف کنم تا بتونم راحت مدیریتشون کنم. مثل مسیر زیر:
    Views/Shared/Components/SubFolder1/SubFolder2/[NameOfComponent]/Default.cshtml 
    ولی وقتی این کار رو می‌کنم ویوها رو در هنگام اجرا نمی‌تونه پیدا کنه؟ چکار باید بکنم؟
    • #
      ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۱۸:۵۰
      روش مسیر دهی مطلق همیشه و در همه جا کار می‌کند:
      return View(viewName: "~/Areas/AreaName/Views/Shared/Components/OnlineUsers/Default.cshtml",model: model);
      • #
        ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۱۹:۵۸
        من از این مورد استفاده کردم ولی جوابگو نبود. توی لاگ دنبال ویو با آدرس شبیه آدرس زیر میگشت:
        Views/Shared/Components/[NameOfComponent]/SubFolder1/SubFolder2/[NameOfComponent]/Default.cshtml 
        یعنی هر آدرسی هم استفاده کردم Views/Shared/Components/[NameOfComponent] رو اولش اضافه کرد
        • #
          ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۲۰:۲۷
          در مثالی که زدم به /~ دقت کنید. /~ در اکثر نگارش‌های ASP.NET یعنی شروع به جستجو از پوشه‌ی ریشه‌ی برنامه.
          • #
            ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۲۰:۴۴
            من در مورد این موضوع کاملا دقت کردم
            عکس زیر تنظیمات و پیغام خطا را به صورت کامل نشون میده

            • #
              ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۲۲:۰۹
              همانطور که در مثال آزمایش شده ارسالی عنوان شد، هم ذکر /~ ضروری است و هم ذکر پسوند فایل. چون مسیر فایل را مشخص می‌کنید که باید به یک فایل واقعی ختم شود.
    • #
      ‫۶ سال و ۲ ماه قبل، دوشنبه ۱۱ تیر ۱۳۹۷، ساعت ۱۹:۰۶
      از R4MVC  میتونین استفاده برای جلوگیری از اشتباه شدن ذکر مسیرها.
  • #
    ‫۲ سال و ۱۰ ماه قبل، سه‌شنبه ۱۱ آبان ۱۴۰۰، ساعت ۱۴:۵۵
    با سلام 
    یه PartialView دارم که تو Layout فراخوانیش میکنم
                    <partial name="_Menu" />
    و توی PartialView میام ViewComponent رو فراخوانی میکنم
                    @await Component.InvokeAsync("NavBar");
    کد ViewComponent
    public class NavbarViewComponent : ViewComponent
        {
            private readonly DbSet<Navbar> _navbars;
            private readonly AppDbContext _dbContext;
    
            public NavbarViewComponent(AppDbContext dbContext)
            {
                _dbContext = dbContext;
                _navbars = _dbContext.Set<Navbar>();
            }
            public async Task<IViewComponentResult> InvokeAsync()
            {
                var model = await _navbars.ToListAsync();
                return View(viewName: "~/Views/Shared/Components/NavbarViewComponent/Default.cshtml", model);
            }
        }
    اما بعد اجرا خطا دارم !

    وقتی از <vc:navbar></vc:navbar>  استفاده میکنم خطایی ندارم ولی ViewComponent اجرا نمیشه !!

    • #
      ‫۲ سال و ۱۰ ماه قبل، سه‌شنبه ۱۱ آبان ۱۴۰۰، ساعت ۱۵:۵۵
      «....  یک نکته: اگر نام کلاسی به ViewComponent  ختم شده بود، نیازی نیست تا ViewComponent  را هم در حین ساخت پوشه‌ی آن ذکر کرد... »
      ViewComponent را هم از نام کلاس و هم از نام پوشه حذف و بر این اساس مسیرها را اصلاح کنید.
  • #
    ‫۲ سال و ۱۰ ماه قبل، چهارشنبه ۱۲ آبان ۱۴۰۰، ساعت ۰۳:۵۵
    نکته تکمیلی :
    ایجاد منوهای چند سطحی با استفاده از ViewComponent تا N سطح
    کلاس Entity :
    public class Navbar
        {
            public int Id { get; set; }
            public string Title { get; set; }
    
            public int? ParentId { get; set; }
            public virtual Navbar Parent { get; set; }
            public bool IsActive { get; set; }
            public bool HasChiled { get; set; }
            public bool IsMegaMenu { get; set; }
            public PageGroup PageGroup { get; set; }
            public string Url { get; set; }
            public bool OpenNewPage { get; set; }
    
            public virtual ICollection<Navbar> Children { get; set; }
        }
    کلاس ViewComponent :
        public class TopNavbar : ViewComponent
        {
            private readonly DbSet<Navbar> _navbars;
            private readonly AppDbContext _dbContext;
    
            public TopNavbar(AppDbContext dbContext)
            {
                _dbContext = dbContext;
                _navbars = _dbContext.Set<Navbar>();
            }
            public async Task<IViewComponentResult> InvokeAsync()
            {
                var navbars = await _navbars.Include(p=>p.Parent).Include(x=>x.Children).OrderBy(x=>x.ParentId).ToListAsync();
                return View(viewName: "~/Views/Shared/Components/NavbarViewComponent/_Menu.cshtml", navbars);
            }
        }
    فراخوانی viewcomponent در Layout.cshtml
     <ul class="menu">
                    <li>
                        <a href="Index_demo6.html"><i class="menu_icon_wrapper fal fa-home-lg-alt"></i>صفحه اصلی</a>
                    </li>
                    @await Component.InvokeAsync("TopNavbar");                
     </ul>
    Menu.cshtml_ :
    @using TR.Context.Entities
    @using Microsoft.AspNetCore.Html
    @model IEnumerable<TR.Context.Entities.Navbar>
    
    
    @foreach (var menu in Model.Where(x => x.Parent == null))
    {
    
        <li class="@(menu.HasChiled ? "has_sub narrow" : "")">
            <a href="#">@menu.Title</a>
            @if (menu.HasChiled)
            {
                <div class="second">
                    <div class="inner">
                        <ul>
                            @foreach (var menuChild in menu.Children)
                            {
                            <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
                            }
                        </ul>
                    </div>
                </div>
            }
        </li>
    }
    پارشیال ویو SubMenu.cshtml_
    @model TR_.Context.Entities.Navbar
    
    <li class="@(Model.HasChiled ? "sub":"")">
        <a href="#">
            @if (Model.Children.Any())
            {<i class="q_menu_arrow fal fa-angle-left"></i>}
            @Model.Title
        </a>
        @if (Model.Children.Any())
        {
            <ul>
                @foreach (var menuChild in Model.Children)
                {
                    <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
                }
            </ul>
        }
    </li>
    با این خروجی :