مطالب
کوئری نویسی در EF Core - قسمت هشتم - کوئری‌های بازگشتی
جدول اعضای این مجموعه، خود ارجاع دهنده طراحی شده‌است:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

       // ...
    }
}
در اینجا RecommendedBy، یک کلید خارجی نال پذیر است که به Id همین جدول اشاره می‌کند. دو خاصیت دیگر تعریف شده، مکمل این خاصیت عددی، جهت سهولت کوئری نویسی‌های EF-Core هستند که در قسمت‌های قبل نیز تعدادی کوئری را در این زمینه مشاهده کردید؛ مانند:
- تولید لیست کاربرانی که کاربر دیگری را توصیه کرده‌اند.
- تولید لیست کاربران، به همراه توصیه کننده‌ی آن‌ها.
- تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها، بدون استفاده از جوین.
- هر کاربر چه تعداد کاربر دیگری را توصیه کرده‌است؟

در این قسمت تعدادی مثال بازگشتی را می‌خواهیم بررسی کنیم.


مثال 1: رنجیره‌ی توصیه کنندگان کاربر با ID مساوی 27 را محاسبه کنید.

می‌خواهیم بدانیم چه کسی کاربر 27 را توصیه کرده و همچنین این کاربر نیز توسط چه شخص دیگری توصیه شده و به همین ترتیب تا بالاترین سطح ممکن.

روش معمول انجام این نوع کوئری‌ها استفاده از «WITH Hierachy» است. اما اگر بخواهیم بدون SQL نویسی مستقیم اینکار را انجام دهیم، می‌توان به صورت زیر عمل کرد:
var id = 27;
var entity27WithAllOfItsParents =
                        context.Members
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree


این کوئری ابتدا تمام رکوردهای جدول کاربران را لیست می‌کند. سپس خاصیت Recommender هر کدام را تا n سطح مقدار دهی می‌کند (خود EF-Core اینکار را انجام می‌دهد). تمام این اتفاقات تا قسمت ToList آن رخ می‌دهند. پس از آن یک FirstOrDefault سمت کاربر را هم داریم (LINQ to Objects). هدف از آن، بازگشت تنها ریشه‌ی مرتبط با ID=27 است و تمام Recommenderهای متصل به آن. این موارد را در تصویر ذیل بهتر می‌توانید مشاهده کنید:


لیست تمام کاربران وجود دارند. سپس سیزدهمین مورد آن، همان کاربر 27 است که توسط کاربر 20 توصیه شده. کاربر 20 توسط کاربر 5 توصیه شده و کاربر 5 توسط کاربر 1 و پس از آن خاصیت Recommender نال است که به معنای پایان پیمودن این زنجیره‌است.
بنابراین مرحله‌ی بعدی پس از یافتن ریشه‌ی کاربر 27، پیمودن خاصیت‌های Recommender به صورت بازگشتی است؛ کاری شبیه به متد FindParents زیر:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindParents(Member member, List<dynamic> actualResult)
        {
            if (member == null || member.Recommender == null)
            {
                return;
            }

            var item = member.Recommender;
            actualResult.Add(new { Recommender = item.MemId, item.FirstName, item.Surname });

            if (item.Recommender != null)
            {
                FindParents(item, actualResult);
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindParents(entity27WithAllOfItsParents, actualResult);


مثال 2: زنجیره‌ی توصیه شده‌های توسط کاربر با ID مساوی 1 را محاسبه کنید.

می‌خواهیم بدانیم کاربر 1، چه کسی را توصیه کرده و این کاربر نیز چه کاربر دیگری را توصیه کرده و به همین ترتیب تا پایین‌ترین سطح ممکن.
var id = 1;
var entity1WithAllOfItsDescendants =
                        context.Members
                            .Include(member => member.Children)
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree
این کوئری نیز شبیه به کوئری مثال قبلی است؛ با یک تفاوت. در اینجا Include(member => member.Children) هم ذکر شده‌است. هدف این است که EF-Core، خاصیت Children را تا n سطح ممکن به صورت خودکار مقدار دهی کند و این مورد دقیقا هدف اصلی مثال جاری است.
وجود Include، سبب تولید یک چنین کوئری می‌شود که در آن جدول کاربران با خودش جوین شده‌است:
SELECT   [m].[MemId],
         [m].[Address],
         [m].[FirstName],
         [m].[JoinDate],
         [m].[RecommendedBy],
         [m].[Surname],
         [m].[Telephone],
         [m].[ZipCode],
         [m0].[MemId],
         [m0].[Address],
         [m0].[FirstName],
         [m0].[JoinDate],
         [m0].[RecommendedBy],
         [m0].[Surname],
         [m0].[Telephone],
         [m0].[ZipCode]
FROM     [Members] AS [m]
         LEFT OUTER JOIN
         [Members] AS [m0]
         ON [m].[MemId] = [m0].[RecommendedBy]
WHERE    ([m].[MemId] = 1)
         OR EXISTS (SELECT 1
                    FROM   [Members] AS [m1]
                    WHERE  ([m].[MemId] = [m1].[RecommendedBy])
                           AND ([m].[MemId] = [m1].[RecommendedBy]))
ORDER BY [m].[MemId], [m0].[MemId];
پس از آن باید خاصیت member.Children را تا هر سطح ممکن به صورت بازگشتی پیمود تا به جواب اصلی این مثال رسید:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindChildren(Member member, List<dynamic> actualResult)
        {
            if (member == null)
            {
                return;
            }

            foreach (var item in member.Children)
            {
                actualResult.Add(new { item.MemId, item.FirstName, item.Surname });
                if (item.Children != null)
                {
                    FindChildren(item, actualResult);
                }
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindChildren(entity1WithAllOfItsDescendants, actualResult);


کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
نظرات مطالب
مقدار دهی اولیه‌ی بانک اطلاعاتی توسط Entity framework Core
طبق کدهای زیر که در پراپرتیهای کلاس owned استفاده شده هیچ وقت نال ثبت نمیشود و بجای نال صفر ثبت میشود.
public double TotalRating { get ; set ; } = 0.0 ; 
public int TotalRaters { get ; set ; } = 0 ; 
public double AverageRating { get ; set ; } = 0.0 ;
 در هیچ یک از عملیات Add,Edit ایرادی ندارد. فقط هنگام حذف آن محصول را حذف نمیکند ولی اگر از کلاس owned شده استفاده نکنم تمام عملیات بدرستی انجام میشود. آیا برای ownesOne درست نوشتم:
modelBuilder.Entity<Product>().OwnsOne(p = > p.Rating)
مطالب دوره‌ها
ساخت یک Mini ORM با AutoMapper
Mini ORM‌ها برخلاف ORMهای کاملی مانند Entity framework یا NHibernate، کوئری‌های LINQ را تبدیل به SQL نمی‌کنند. در اینجا کار با SQL نویسی مستقیم شروع می‌شود و مهم‌ترین کار این کتابخانه‌ها، نگاشت نتیجه‌ی دریافتی از بانک اطلاعاتی به اشیاء دات نتی هستند. خوب ... AutoMapper هم دقیقا همین کار را انجام می‌دهد! بنابراین در ادامه قصد داریم یک Mini ORM را به کمک AutoMapper طراحی کنیم.


کلاس پایه AdoMapper

public abstract class AdoMapper<T> where T : class
{
    private readonly SqlConnection _connection;
 
    protected AdoMapper(string connectionString)
    {
        _connection = new SqlConnection(connectionString);
    }
 
    protected virtual IEnumerable<T> ExecuteCommand(SqlCommand command)
    {
        command.Connection = _connection;
        command.CommandType = CommandType.StoredProcedure;
        _connection.Open();
 
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                return Mapper.Map<IDataReader, IEnumerable<T>>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
 
    protected virtual T GetRecord(SqlCommand command)
    {
        command.Connection = _connection;
        _connection.Open();
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                reader.Read();
                return Mapper.Map<IDataReader, T>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
 
    protected virtual IEnumerable<T> GetRecords(SqlCommand command)
    {
        command.Connection = _connection;
        _connection.Open();
        try
        {
            var reader = command.ExecuteReader();
            try
            {
                return Mapper.Map<IDataReader, IEnumerable<T>>(reader);
            }
            finally
            {
                reader.Close();
            }
        }
        finally
        {
            _connection.Close();
        }
    }
}
در اینجا کلاس پایه Mini ORM طراحی شده را ملاحظه می‌کنید. برای نمونه قسمت GetRecords آن مانند مباحث استاندارد ADO.NET است. فقط کار خواندن و همچنین نگاشت رکوردهای دریافت شده از بانک اطلاعاتی به شیء‌ایی از نوع T توسط AutoMapper انجام خواهد شد.


نحوه‌ی استفاده از کلاس پایه AdoMapper

در کدهای ذیل نحوه‌ی ارث بری از کلاس پایه AdoMapper و سپس استفاده از متدهای آن‌را ملاحظه می‌کنید:
public class UsersService : AdoMapper<User>, IUsersService
{
    public UsersService(string connectionString)
        : base(connectionString)
    {
    }
 
    public IEnumerable<User> GetAll()
    {
        using (var command = new SqlCommand("SELECT * FROM Users"))
        {
            return GetRecords(command);
        }
    }
 
    public User GetById(int id)
    {
        using (var command = new SqlCommand("SELECT * FROM Users WHERE Id = @id"))
        {
            command.Parameters.Add(new SqlParameter("id", id));
            return GetRecord(command);
        }
    }
}
در این مثال نحوه‌ی تعریف کوئری‌های پارامتری نیز در متد GetById به نحو متداولی مشخص شده‌است. کار نگاشت حاصل این کوئری‌ها به اشیاء دات نتی را AutoMapper انجام خواهد داد. نحوه‌ی کار نیز، نگاشت فیلد f1 به خاصیت f1 است (هم نام‌ها به هم نگاشت می‌شوند).


تعریف پروفایل مخصوص AutoMapper

ORMهای تمام عیار، کار نگاشت فیلدهای بانک اطلاعاتی را به خواص اشیاء دات نتی، به صورت خودکار انجام می‌دهند. در اینجا همانند روش‌های متداول کار با AutoMapper نیاز است این نگاشت را به صورت دستی یکبار تعریف کرد:
public class UsersProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<IDataRecord, User>();
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
و سپس در ابتدای برنامه آن‌را به AutoMapper معرفی نمود:
Mapper.Initialize(cfg => // In Application_Start()
{
    cfg.AddProfile<UsersProfile>();
});


سفارشی سازی نگاشت‌های AutoMapper

فرض کنید کلاس Advertisement زیر، معادل است با جدول Advertisements بانک اطلاعاتی؛ با این تفاوت که در کلاس تعریف شده، خاصیت TitleWithOtherName تطابقی با هیچکدام از فیلدهای بانک اطلاعاتی ندارد. بنابراین اطلاعاتی نیز به آن نگاشت نخواهد شد.
public class Advertisement
{
    public int Id { set; get; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int UserId { get; set; }
 
    public string TitleWithOtherName { get; set; }
}
برای رفع این مشکل می‌توان حین تعریف پروفایل مخصوص Advertisement، آن‌را سفارشی سازی نیز نمود:
public class AdvertisementsProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<IDataRecord, Advertisement>()
            .ForMember(dest => dest.TitleWithOtherName,
                       options => options.MapFrom(src =>
                            src.GetString(src.GetOrdinal("Title"))));
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
در اینجا پس از تعریف نگاشت مخصوص کار با IDataRecordها، عنوان شده‌است که هر زمانیکه به خاصیت TitleWithOtherName رسیدی، مقدارش را از فیلد Title دریافت و جایگزین کن.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
سازمان پروژه‌های پژوهشی پیشرفتهٔ دفاعی (DARPA): تمام کدهای C باید به Rust تبدیل شوند!

DARPA Wants All C Converted To Rust

After more than two decades of grappling with memory safety issues in C and C++, the software engineering community has reached a consensus. It’s not enough to rely on bug-finding tools. The preferred approach is to use “safe” programming languages that can reject unsafe programs at compile time, thereby preventing the emergence of memory safety issues.

سازمان پروژه‌های پژوهشی پیشرفتهٔ دفاعی (DARPA): تمام کدهای C باید به Rust تبدیل شوند!
مطالب
مراحل ارسال یک پروژه‌ی Visual Studio به GitHub
از نگارش 2012 ویژوال استودیو، امکان کار با مخازن Git، به صورت یکپارچه و توکار و بدون نیاز به ابزارهای جانبی، توسط آن فراهم شده‌است. در ادامه قصد داریم به کمک این ویژگی توکار، نحوه‌ی ارسال یک پروژه‌ی از پیش موجود VS.NET را برای اولین بار به GitHub بررسی کنیم.


تنظیمات مقدماتی GitHub

در ابتدا نیاز است یک مخزن کد خالی را در GitHub ایجاد کنید. برای این منظور به برگه‌ی Repositories در اکانت GitHub خود مراجعه کرده و بر روی دکمه‌ی New کلیک کنید:


سپس در صفحه‌ی بعدی، نام پروژه را به همراه توضیحاتی وارد نمائید و بر روی دکمه‌ی Create repository کلیک کنید. در اینجا سایر گزینه‌ها را انتخاب نکنید. نیازی به انتخاب گزینه‌ی READ ME و یا انتخاب مجوز و غیره نیست. تمام این کارها را در سمت پروژه‌ی اصلی می‌توان انجام داد و یا VS.NET فایل‌های ignore را به صورت خودکار ایجاد می‌کند. در اینجا صرفا هدف، ایجاد یک مخزن کد خالی است.


از اطلاعات صفحه‌ی بعدی، تنها به آدرس مخصوص GitHub آن نیاز داریم. از این آدرس در VS.NET برای ارسال اطلاعات به سرور استفاده خواهیم کرد:



تنظیمات VS.NET برای ارسال پروژه به مخزن GitHub

پس از ایجاد یک مخزن کد خالی در GitHub، اکنون می‌توانیم پروژه‌ی خود را به آن ارسال کنیم. برای این منظور از منوی File، گزینه‌ی Add to source control را انتخاب کنید و در صفحه‌ی باز شده، گزینه‌ی Git را انتخاب نمائید:



سپس در کنار برگه‌ی Solution Explorer، برگه‌ی Team Explorer را انتخاب کنید. در اینجا بر روی دکمه‌ی Home در نوار ابزار آن کلیک کرده و سپس بر روی دکمه‌ی Unsynced commits کلیک نمائید.


در ادامه در صفحه‌ی باز شده، همان آدرس مخصوص مخزن کد جدید را در GitHub وارد کرده و بر روی دکمه‌ی Publish کلیک کنید:


در اینجا بلافاصله صفحه‌ی لاگینی ظاهر می‌شود که باید در آن مشخصات اکانت GitHub خود را وارد نمائید:


به این ترتیب عملیات Publish اولیه انجام شده و تصویر ذیل نمایان خواهد شد:


در اینجا بر روی دکمه‌ی Sync کلیک کنید. به این ترتیب مخزن کد GitHub به پروژه‌ی جاری متصل خواهد شد:


سپس نیاز است فایل‌های موجود را به مخزن کد GitHub ارسال کرد. بنابراین پس از مشاهده‌ی پیام موفقیت آمیز بودن عملیات همگام سازی، بر روی دکمه‌ی Home در نوار ابزار کلیک کرده و اینبار گزینه‌ی Changes را انتخاب کنید:


در اینجا پیام اولین ارسال را وارد کرده و سپس بر روی دکمه‌ی Commit کلیک کنید:


پس از مشاهده‌ی پیام موفقیت آمیز بودن commit محلی، نیاز است تا آن‌را با سرور نیز هماهنگ کرد. به همین جهت در اینجا بر روی لینک Sync کلیک کرده و در صفحه‌ی بعدی بر روی دکمه‌ی Sync کلیک کنید:



اندکی صبر کنید تا فایل‌ها به سرور ارسال شوند. اکنون اگر به GitHub مراجعه کنید، فایل‌های ارسالی قابل مشاهده هستند:



اعمال تغییرات بر روی پروژه‌ی محلی و ارسال به سرور

در ادامه می‌خواهیم دو فایل README.md و LICENSE.md را به پروژه اضافه کنیم. پس از افزودن آن‌ها، یا هر تغییر دیگری در پروژه، اینبار برای ارسال تغییرات به سرور، تنها کافی است به برگه‌ی Team explorer مراجعه کرده و ابتدا بر روی دکمه‌ی Home کلیک کرد تا منوی انتخاب گزینه‌‌های آن ظاهر شود. در اینجا تنها کافی است گزینه‌ی Changes را انتخاب و دقیقا همان مراحل عنوان شده‌ی پیشین را تکرار کرد. ابتدا ورود پیام Commit و سپس Commit. در ادامه Sync محلی و سپس Sync با سرور.
مطالب
استفاده از لنگر (anchor) برای اسکرول به قسمت خاصی از صفحه در Blazor Server
فرض کنید کدی مانند زیر را در یک کامپوننت داریم و انتظار این است که با کلیک بر روی Section2، به بخش مورد نظر اسکرول شویم:
@page "/test"

<nav>
    <!-- یک روش -->
    <a href="#section2">Section2</a>

    <!-- روش دیگر -->
    <NavLink href="#section2">Section2</NavLink>    
</nav>

@* ... *@


<h2 id="section2">It's Section2.</h2>
@* ... *@
اما متاسفانه در Blazor Server تا نسخه فعلی آن (نسخه هفت)، این کار ساده به راحتی امکان‌پذیر نیست. همانطور که ملاحظه می‌کنید، به دو روش، نویگیشن انجام شده‌است؛ اما هیچ‌یک ما را به هدف نمی‌رسانند. دلیل این موضوع، رفتار Blazor Server در بارگذاری صفحات می‌باشد. در حقیقت المان‌ها موقع بارگذاری، هنوز در صفحه وجود ندارند. در واقع ابتدا نیاز است که اتصال SignalR برقرار شود و سپس داده‌ها از سرور دریافت شوند (مگر در حالت pre-rendered که مشکلات خاص خود را در پی دارد).
برای انجام این کار دو روش وجود دارد؛ یکی بر پایه‌ی جاوااسکریپت است و دیگری توسط توابع داخلی Blazor JS.


روش جاوااسکریپتی

ابتدا یک کامپوننت را به نام AnchorNavigation ایجاد می‌نماییم:
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}
سپس کد جاوا اسکریپتی زیر را در جایی قبل از فراخوانی <script src="_framework/blazor.server.js"></script> قرار می‌دهیم (برای مثال اگر می‌خواهیم در اکثر صفحات از آن بهره ببریم، آن را در layout.cshtmlـ قرار می‌دهیم).
function BlazorScrollToId(id) {
            const element = document.getElementById(id);
            if (element instanceof HTMLElement) {
                element.scrollIntoView({
                    behavior: "smooth",
                    block: "start",
                    inline: "nearest"
                });
            }
        }
حال در هر کامپوننتی که نیاز به استفاده از لنگر (anchor) داریم، به شکل زیر عمل می‌کنیم:
@page "/"

<PageTitle>Index</PageTitle>

<a href="#section2">
    <h1>Section2</h1>
</a>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<div id="section2">
    <h2>It's Section2. </h2>
</div>

<AnchorNavigation />


روش استفاده از توابع داخلی Blazor JS 

می توان از ElementReference و FocusAsync که در حقیقت مربوط به خود Blazor JS می‌باشند استفاده نمود. اینبار کدهای کامپوننت AnchorNavigation را به شکل زیر تغییر می‌دهیم:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using System.Diagnostics.CodeAnalysis;

namespace TestAnchorNavigation;

public class AnchorNavigation: ComponentBase, IDisposable
{
    private bool _setFocus;

    [Inject] private NavigationManager NavManager { get; set; } = default!;
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public string? BookmarkName { get; set; }
    [DisallowNull] public ElementReference? Element { get; private set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "span");
        builder.AddAttribute(2, "tabindex", "-1");
        builder.AddContent(3, this.ChildContent);
        builder.AddElementReferenceCapture(4, this.SetReference);
        builder.CloseElement();
    }

    protected override void OnInitialized()
        => NavManager.LocationChanged += this.OnLocationChanged;

    protected override void OnParametersSet()
        => _setFocus = this.IsMe();

    private void SetReference(ElementReference reference)
        => this.Element = reference;

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        if (this.IsMe())
        {
            _setFocus = true;
            this.StateHasChanged();
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (_setFocus)
            await this.Element!.Value.FocusAsync(false);

        _setFocus = false;
    }

    private bool IsMe()
    {
        string? elementId = null;

        var uri = new Uri(this.NavManager.Uri, UriKind.Absolute);
        if (uri.Fragment.StartsWith('#'))
        {
            elementId = uri.Fragment.Substring(1);
            return elementId == BookmarkName;
        }
        return false;
    }

    public void Dispose()
        => NavManager.LocationChanged -= this.OnLocationChanged;
}
و پیرو آن، صفحه‌ی موردنظر برای استفاده از لنگر نیز به شکل زیر تغییر خواهد کرد:
@page "/"

<PageTitle>Index</PageTitle>

<NavLink href="#section2">
    <h1>Section2</h1>
</NavLink>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<AnchorNavigation BookmarkName="section2">
      <h2>It's Section2. </h2>
</AnchorNavigation>
نظرات مطالب
دستکاری کردن عملیات Sort در SQL Server
با سلام یک خطا خوردم
ORDER BY 
CASE WHEN @IdOrderList = 1 THEN bb.Inventory END DESC,
CASE WHEN @IdOrderList = 2 THEN bb.TotalPrice END ASC,
CASE WHEN @IdOrderList = 3 THEN bb.TotalPrice END DESC,
CASE WHEN @IdOrderList = 4 THEN bb.Inventory END ASC,

CASE WHEN @IdOrderList = 5 THEN bb.Inventory END DESC,
CASE WHEN @IdOrderList = 5 THEN bb.TotalPrice END ASC,

--CASE WHEN @IdOrderList = 6 THEN bb.Inventory END DESC,
--CASE WHEN @IdOrderList = 6 THEN bb.TotalPrice END DESC

CASE @IdOrderList WHEN 5 THEN bb.Inventory END DESC, bb.TotalPrice,
CASE @IdOrderList WHEN 6 THEN bb.Inventory END DESC, bb.TotalPrice DESC

OFFSET (@PageIndex-1)*20 ROWS
FETCH NEXT 20 ROWS ONLY
A column has been specified more than once in the order by list. Columns in the order by list must be unique. 
مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SQLite به صورت توکار از full-text search پشتیبانی می‌کند؛ اما اهمیت آن چیست؟ هدف از full-text search، انجام جستجوهای بسیار سریع، در ستون‌های متنی یک جدول بانک اطلاعاتی است. بدون وجود یک چنین قابلیتی، عموما برای انجام اینکار از دستور LIKE استفاده می‌شود:
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
کار این کوئری، یافتن ردیف‌هایی است که در آن واژه‌ی cat وجود دارند. مشکل این روش، عدم استفاده‌ی از ایندکس‌ها و اصطلاحا انجام یک full table scan است. با استفاده از دستور LIKE، باید تک تک ردیف‌های بانک اطلاعاتی برای یافتن واژه‌ی مدنظر، اسکن و بررسی شوند و انجام اینکار با بالا رفتن تعداد رکوردهای بانک اطلاعاتی، کندتر و کندتر خواهد شد. برای رفع این مشکل، راه حلی به نام full-text search ارائه شده‌است که کار آن ایندکس کردن تمام ستون‌های متنی مدنظر و سپس جستجوی بر روی این ایندکس از پیش آماده شده‌است.
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
بنابراین هدف از این سری، جایگزین کردن متدهای الحاقی Contains ، StartsWith و EndsWith، با روشی بسیار سریعتر است.

یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه می‌شود (که آن نیز یک full table scan است):
SELECT  "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
اما دقیقا دستور Like را به همراه متدهای الحاقی StartsWith و یا EndsWith می‌توان مشاهده کرد:
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList();
// SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList();
// SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')


معرفی موجودیت‌های مثال این سری

هدف اصلی ما، ایندکس کردن full-text ستون‌های متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic;

namespace EFCoreSQLiteFTS.Entities
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Chapter> Chapters { get; set; }
    }

    public class Chapter
    {
        public int Id { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        public User User { get; set; }
        public int UserId { get; set; }
    }
}


ایجاد جدول مجازی Full-text search

زمانیکه عملیات Migration را در EF Core فعال و اجرا می‌کنیم، دو جدول متناظر با Chapter و User ایجاد می‌شوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکس‌های تشکیل شده‌ی از ستون‌های متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته می‌شود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارش‌های مختلفی از موتور full-text search آن منتشر شده‌اند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 می‌باشد و به همراه توزیعی که مایکروسافت ارائه می‌دهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده می‌کنید:
CREATE VIRTUAL TABLE "Chapters_FTS"
USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
جدول مجازی، با اجرای دستور CREATE VIRTUAL TABLE  ایجاد می‌شود و USING fts5 آن به معنای استفاده‌ی از موتور full-text search نگارش پنجم آن است. سپس لیست ستون‌هایی را که می‌خواهیم ایندکس کنیم، ذکر می‌شوند؛ مانند Text و Title در اینجا. همانطور که مشاهده می‌کنید، فقط نام این ستون‌ها قابل تعریف هستند و هیچ نوع اطلاعات اضافه‌تری را نمی‌توان ذکر کرد.
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل داده‌های مرتبط با ستون‌های ذکر شده نیز ذخیره شوند و آن‌ها را می‌توان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شده‌است. content_rowid به primary key جدول content اشاره می‌کند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش می‌دهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانه‌ی اصل اطلاعات متناظر با ایندکس‌های FTS نیست.

اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامه‌های مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context)
{
    // For SQLite FTS
    // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
    context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");
}
فقط کافی است در ابتدای اجرای برنامه با استفاده از متد ExecuteSqlRaw، عبارت SQL متناظر با ایجاد جدول مجازی را اجرا کنیم. این یک روش ایجاد این نوع جداول است؛ روش دیگر آن، قرار دادن همین قطعه کد در متد "protected override void Up(MigrationBuilder migrationBuilder)" مربوط به کلاس‌های ایجاد شده‌ی توسط عملیات Migration است.


به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها

پس از اجرای دستورCREATE VIRTUAL TABLE  فوق، SQLite پنج جدول را به صورت خودکار ایجاد می‌کند که در تصویر زیر قابل مشاهده هستند:


البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکس‌های full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت می‌شوند.

اما ... نکته‌ی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شده‌است، اما تغییرات آن‌را ردیابی نمی‌کند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ می‌دهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمی‌شود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شده‌ی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it.
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
در اینجا ابتدا روش ایجاد یک جدول جدید و سپس ایجاد یک جدول مجازی FTS را از روی آن مشاهده می‌کنید.
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامه‌های خود کار می‌کنیم، تعریف شده‌اند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام می‌دهند.
همانطور که مشاهده می‌کنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آن‌را ذکر نمی‌کنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.

نکته‌ی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده می‌شوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعه‌ی بعدی که برنامه را اجرا می‌کنید، خطای «این بانک اطلاعاتی تخریب شده‌است» را مشاهده کرده (database disk image is malformed) و دیگر نمی‌توانید با فایل بانک اطلاعاتی خود کار کنید.


به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core

روش تعریف تریگرهای یاد شده، مستقل از EF Core بوده و راسا توسط خود بانک اطلاعاتی مدیریت می‌شود. بنابراین فقط کافی است دستور CREATE TRIGGER را به همان نحوی که عنوان شد، توسط متد ExecuteSqlRaw اجرا کنیم تا جزئی از ساختار بانک اطلاعاتی شوند؛ اما ... این روش برای برنامه‌هایی با متن‌های پیچیده کارآیی ندارد. برای مثال فرض کنید اطلاعات اصلی شما با فرمت HTML است. ایندکس ایجاد شده، تگ‌های HTML را حذف نمی‌کند و آن‌ها را نیز ایندکس می‌کند که نه تنها سبب بالا رفتن حجم بانک اطلاعاتی می‌شود، بلکه زمانیکه ما قصد جستجویی را بر روی اطلاعات HTML ای داریم، اساسا کاری به تگ‌های آن نداشته و هدف اصلی ما، متن‌های درج شده‌ی در آن است. نمونه‌ی دیگر آن داشتن اطلاعاتی با «اعراب» است و یا شاید نیاز به یک‌دست سازی ی و ک فارسی وجود داشته باشد. به این نوع عملیات، «نرمال سازی متن» گفته می‌شود و با روش تریگرهای فوق قابل تعریف و مدیریت نیست. به همین جهت می‌توان از روش پیشنهادی زیر استفاده کرد:

الف) یافتن لیست اطلاعات تغییر یافته‌ی حاصل از اعمال insert/update/delete
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class EFChangeTrackerExtensions
    {
        public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)>
                    GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new()
        {
            if (!dbContext.ChangeTracker.AutoDetectChangesEnabled)
            {
                // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene.
                dbContext.ChangeTracker.DetectChanges();
            }

            return dbContext.ChangeTracker.Entries<TEntity>()
                    .Where(IsEntityChanged)
                    .Select(entityEntry => (entityEntry.State,
                                            entityEntry.Entity,
                                            createWithValues<TEntity>(entityEntry.OriginalValues)))
                    .ToList();
        }

        private static bool IsEntityChanged(EntityEntry entry)
        {
            return entry.State == EntityState.Added
                    || entry.State == EntityState.Modified
                    || entry.State == EntityState.Deleted
                    || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry));
        }

        private static T createWithValues<T>(PropertyValues values) where T : new()
        {
            var entity = new T();
            foreach (var prop in values.Properties)
            {
                var value = values[prop.Name];
                if (value is PropertyValues)
                {
                    throw new NotSupportedException("nested complex object");
                }
                else
                {
                    prop.PropertyInfo.SetValue(entity, value);
                }
            }
            return entity;
        }
    }
}
هدف از متد GetChangedEntities فوق این است که با استفاده از سیستم tracking، نوع عملیات انجام شده و همچنین اصل موجودیت‌ها را پیش و پس از تغییر، بتوان لیست کرد و سپس بر اساس آن‌ها، جدول مجازی FTS را به روز رسانی نمود.
علت نیاز به نمونه‌ی اصل و سپس تغییر کرده‌ی موجودیت‌ها، به نحوه‌ی تعریف تریگرهای مخصوص به به روز رسانی FTS بر می‌گردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آن‌ها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.

ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using EFCoreSQLiteFTS.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class FtsNormalizer
    {
        private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled);

        public static string NormalizeText(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return string.Empty;
            }

            // Remove html tags
            text = _htmlRegex.Replace(text, string.Empty);

            // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ...

            return text;
        }
    }

    public static class UpdateFtsTriggers
    {
        public static void UpdateChapterFTS(
            this DbContext context,
            List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters)
        {
            var database = context.Database;

            try
            {
                database.BeginTransaction(IsolationLevel.ReadCommitted);

                foreach (var (State, NewEntity, OldEntity) in changedChapters)
                {
                    var chapterNew = NewEntity;
                    var chapterOld = OldEntity;

                    var normalizedNewText = chapterNew.Text.NormalizeText();
                    var normalizedOldText = chapterOld.Text.NormalizeText();
                    var normalizedNewTitle = chapterNew.Title.NormalizeText();
                    var normalizedOldTitle = chapterOld.Title.NormalizeText();
                    switch (State)
                    {
                        case EntityState.Added:
                            if (shouldSkipAddedChapter(chapterNew))
                            {
                                continue;
                            }
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Modified:
                            if (shouldSkipModifiedChapter(chapterNew, chapterOld))
                            {
                                continue;
                            }
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                                        chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Deleted:
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                    chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            break;
                    }
                }
            }
            finally
            {
                database.CommitTransaction();
            }
        }

        private static bool shouldSkipAddedChapter(Chapter chapterNew)
        {
            // TODO: add your logic to avoid indexing this item
            return false;
        }

        private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld)
        {
            // TODO: add your logic to avoid indexing this item
            return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title;
        }
    }
}
در اینجا نحوه‌ی تعریف متد UpdateChapterFTS را مشاهده می‌کند که اطلاعات خودش را از متد GetChangedEntities دریافت کرده و سپس یکی یکی آن‌ها را در جدول مجازی FTS، با فرمت مخصوصی که عنوان شد (دقیقا متناظر با فرمت تریگرهای مستندات رسمی FTS)، درج می‌کند.
همچنین در اینجا متد NormalizeText را نیز مشاهده می‌کند که بر روی ستون‌های متنی اعمال شده‌است. کار آن پاکسازی تگ‌های یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود می‌توان منطق‌های پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج می‌شوند، زمانیکه بر روی آن‌ها جستجویی صورت می‌گیرد، دیگر شامل جستجوی بر روی تگ‌های HTML ای نیست و دقت بسیار بیشتری دارد.

ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آن‌ها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Chapter> Chapters { get; set; }
        public DbSet<User> Users { get; set; }

        public override int SaveChanges()
        {
            var changedChapters = this.GetChangedEntities<Chapter>();

            this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges();
            this.ChangeTracker.AutoDetectChangesEnabled = true;

            this.UpdateChapterFTS(changedChapters);
            return result;
        }
    }
}
از این پس تمام عملیات insert/update/delete برنامه تحت کنترل قرار گرفته و به صورت خودکار سبب به روز رسانی جدول مجازی FTS نیز می‌شوند.


در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی می‌کنیم.
مطالب
Vue.js - بحث Event Handling - آشنایی و شیوۀ استفاده از رویدادها - قسمت پنجم
در این قسمت به بحث رویدادها می‌پردازیم و اینکه به چه صورتی می‌توانیم از آن‌ها درون پروژه استفاده کنیم. فریم‌ورک Vue.js در عین سادگی می‌تواند نیاز شما را برآورده کرده و به نحو مطلوبی خروجی مناسبی را بدون هیچ دردسری، به شما تحویل دهد.
رویدادها یا همان eventها به ما این اختیار را می‌دهند که بتوانیم عمل خاصی را بر روی یک صفحه یا قسمت‌های مختلف درون پروژه اعمال کنیم و کاملا شبیه رویدادهایی است که در زبان‌های دیگر با آن کار کرده و آشنایی دارید.
برای شروع کار ابتدا نیاز است تا همانند قسمت‌های قبلی، کدهای صفحه مورد نظر خود را بنویسم و صفحه مورد نظر را ذخیره کنیم. کدها برای شروع کار، بدین شکل است که به صورت زیر نوشته و آماده شده است.
<html>
<head>
    <meta charset="UTF-8">
    <title>dotnet</title>
</head>
    <body id="dotnettips">
 <a v-on:click="message" href="bank.html">click here!</a> 
       <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js">
       </script>
       <script type="text/javascript">
     new Vue({
        el: '#dotnettips',
        data:{
        },
        methods:{
            message: function () {
                alert("hi friends");
            }
        }
    });
        </script>      
        </body>
</html>
در کدهای بالا قسمت body طبق روال همیشگی یک id را به خود اختصاص داده است و لازم است که بدانیم مشخصه‌ی id درون بدنه‌ی vue و مشخصه‌ی el به کدام بخش تعلق گرفته است.
دقت بفرمائید که ما نیاز داریم تا یک function بنویسم و رویداد مورد نظر را اجرا کنیم که جهت آزمایش به دو صفحه html نیاز است و در بخش زیر از کدهای فوق قابل مشاهده است.

vue مورد نظر و متد برای اجرای رویداد بدین شکل نوشته شده است.
  new Vue({
        el: '#dotnettips',
        data:{

        },
        methods:{
            message: function () {
                alert("hi friends");
            }
        }
    });
حال باید رویداد هدایت شود که از تگ a استفاده شده است. به صورت زیر:
<html>

<head>
    <meta charset="UTF-8">
    <title>dotnet</title>
</head>

    <body id="dotnettips">

 <a v-on:click="message" href="bank.html">click here!</a>
لازم به اشاره است که جهت معرفی رویدادی به صفحه دیگر باید از دستور v-on که vuejs در اختیار ما قرار داده است، استفاده کرد و ما نیز چنین کدی را نوشته‌ایم و صفحه مورد نظر را ارجاع داده‌ایم.
  • حتما باید نام متد به رویداد کلیک معرفی شود که در کد فوق قابل مشاهده است.
نام صفحه مورد نظر برای ارجاع : bank.html
دقت شود، درون vue که نوشته شده‌است، یک متد نیز فراخوانی شده است تا رویداد، اجرا شود و ما نام آن متد را message در نظر گرفته‌ایم. 
دو صفحه به دلخواه ساخته شده‌است که نام صفحه اول با نام cc.html و صفحه دوم با نام bank.html ایجاد شده‌اند. زمان اجرای رویداد، به صفحه دوم و دریافت پیغام hi friends، توسط تابعی که نوشته‌ایم و نمایش آن بر روی صفحه مواجه خواهیم شد. بدین معنی است که رویداد به درستی اجرا شده است و سپس به صفحه دوم هدایت می‌شویم.
تصویری از خروجی ضمیمه شده است.

بازخوردهای دوره
پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC
رویداد اسکرول را باید تحت نظر قرار داد و هر زمانیکه برای مثال از یک المان خاص رد شد، کدهای رویداد کلیک را فراخوانی کند:
$(function(){
  $(window).scroll(function(){
    var aTop = $('.elem').height();
    if($(this).scrollTop()>=aTop){
        //todo: ....
    }
  });
});