پاسخ به بازخوردهای پروژهها
در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» با نحوهی ساخت و توزیع برنامههای Angular، در دو حالت محیط توسعه و محیط ارائهی نهایی آشنا شدیم. همچنین در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» نحوهی ترکیب یک برنامهی ASP.NET Core و Angular را بررسی کردیم. در اینجا میخواهیم فایل index.html ایی را که Angular CLI تولید میکند، با فایل Layout برنامههای ASP.NET Core جایگزین کنیم؛ تا بتوانیم در صورت نیاز، سفارشی سازیهای بیشتری را به صفحهی اول سایت اعمال نمائیم.
استفاده از Tag Helpers ویژهی ASP.NET Core برای مدیریت محیطهای توسعه و تولید
فایلهای برنامهی تک صفحهای تولید شدهی توسط Angular CLI، در نهایت یک چنین شکلی را خواهند داشت:
این فایلها نیز در حالت توسعه تهیه شدهاند. در یک برنامهی واقعی، صفحهی سادهی index.html تولیدی آن، تنها میتواند یک قالب شروع به کار باشد و نه فایل نهایی که قرار است ارائه شود. نیاز است به این فایل تگهای بیشتری را اضافه کرد و سفارشی سازیهای خاصی را به آن اعمال نمود. در این حالت با توجه به بازنویسی و تولید مجدد این فایل در هر بار ساخت برنامه، میتوان از فایل Layout پروژهی ASP.NET Core جاری استفاده کرد. به این ترتیب از مزایای Razor و تمام زیرساختی که در اختیار داریم نیز محروم نخواهیم شد.
بنابراین تنها کاری را که باید انجام دهیم، کپی ساختار فایل index.html تولیدی به فایل Layout برنامه است.
مشکل! در حالت توسعه، نام فایلهای تولید شده به همین سادگی است که ملاحظه میکنید. اما در حالت ارائهی نهایی، این فایلها به همراه یک هش نیز تولید میشوند (پیاده سازی مفهوم cache busting و اجبار به بهروز رسانی کش مرورگر، باتوجه به تغییر آدرس فایلها)؛ مانند vendor.ea3f8329096dbf5632af.bundle.js
راه حل اول: تولید فایلهای نهایی بدون هش
در حین ساخت و تولید یک برنامهی Angular CLI در حالت ارائهی نهایی، تنها کافی است سوئیچ output-hashing، به none تنظیم شود، تا این هشها به نام فایلهای تولیدی اضافه نشوند.
درکل بهتر است از این روش استفاده نشود، چون با وجود پروکسیهای کش کردن اطلاعات در بین راه، احتمال اینکه کاربران نگارشهای قدیمی برنامه را مشاهده کنند، بسیار زیاد است.
راه حل دوم: تگ Script در ASP.NET Core اجازهی ذکر تمام فایلهای اسکریپت یک پوشه را نیز میدهد
هرچند این قابلیت جالب است و سبب الحاق یکجای تمام فایلهای js موجود در پوشهی wwwroot خواهد شد، اما پاسخگوی کار ما نخواهد بود؛ چون ترتیب قرارگیری این فایلها مهم است.
راه حل واقعی
در اینجا کدهای کامل فایل Views\Shared\_Layout.cshtml را که میتواند جایگزین فایل index.html تولیدی توسط Angular CLI باشد، ملاحظه میکنید:
در اینجا دو حالت توسعه و همچنین ارائهی نهایی مدنظر قرار گرفتهاند. در حالت توسعه، دقیقا از همان نامهای سادهی تولیدی استفاده شدهاست و در حالت ارائهی نهایی چون نام فایلها به صورت
تولید میشوند، میتوان قسمت هش را با * جایگزین کرد؛ مانند "asp-src-include="~/vendor*.js
همچنین باید دقت داشت که در حالت توسعه، تمام شیوه نامههای برنامه در فایل styles.bundle.js قرار میگیرند. اما در حالت ارائهی نهایی، این فایل وجود نداشته و با نام کلی styles*.css تولید میشود که باید در head صفحه قرار گیرد (مانند تنظیمات حالت تولید در Layout فوق).
اصلاح قسمت URL Rewrite برنامه
در حالت کار با برنامههای تک صفحهای وب، در اولین درخواست رسیدهی به برنامه ممکن است آدرسی درخواست شود که معادل کنترلر و اکشن متدی را در برنامهی سمت سرور نداشته باشد. در این حالت کاربر را به همان صفحهی index.html هدایت میکنیم تا سیستم مسیریابی سمت کلاینت، کار نمایش آن صفحه را انجام دهد:
از آنجائیکه پس از اصلاحات فوق دیگر از این فایل استفاده نمیشود، باید تغییر ذیل را نیز اعمال کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
استفاده از Tag Helpers ویژهی ASP.NET Core برای مدیریت محیطهای توسعه و تولید
فایلهای برنامهی تک صفحهای تولید شدهی توسط Angular CLI، در نهایت یک چنین شکلی را خواهند داشت:
این فایلها نیز در حالت توسعه تهیه شدهاند. در یک برنامهی واقعی، صفحهی سادهی index.html تولیدی آن، تنها میتواند یک قالب شروع به کار باشد و نه فایل نهایی که قرار است ارائه شود. نیاز است به این فایل تگهای بیشتری را اضافه کرد و سفارشی سازیهای خاصی را به آن اعمال نمود. در این حالت با توجه به بازنویسی و تولید مجدد این فایل در هر بار ساخت برنامه، میتوان از فایل Layout پروژهی ASP.NET Core جاری استفاده کرد. به این ترتیب از مزایای Razor و تمام زیرساختی که در اختیار داریم نیز محروم نخواهیم شد.
بنابراین تنها کاری را که باید انجام دهیم، کپی ساختار فایل index.html تولیدی به فایل Layout برنامه است.
مشکل! در حالت توسعه، نام فایلهای تولید شده به همین سادگی است که ملاحظه میکنید. اما در حالت ارائهی نهایی، این فایلها به همراه یک هش نیز تولید میشوند (پیاده سازی مفهوم cache busting و اجبار به بهروز رسانی کش مرورگر، باتوجه به تغییر آدرس فایلها)؛ مانند vendor.ea3f8329096dbf5632af.bundle.js
راه حل اول: تولید فایلهای نهایی بدون هش
ng build -prod --output-hashing=none
درکل بهتر است از این روش استفاده نشود، چون با وجود پروکسیهای کش کردن اطلاعات در بین راه، احتمال اینکه کاربران نگارشهای قدیمی برنامه را مشاهده کنند، بسیار زیاد است.
راه حل دوم: تگ Script در ASP.NET Core اجازهی ذکر تمام فایلهای اسکریپت یک پوشه را نیز میدهد
<script type="text/javascript" asp-src-include="*.js"></script>
راه حل واقعی
در اینجا کدهای کامل فایل Views\Shared\_Layout.cshtml را که میتواند جایگزین فایل index.html تولیدی توسط Angular CLI باشد، ملاحظه میکنید:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/x-icon" href="favicon.ico"> <title>ng2-lab</title> <base href="/"> <environment names="Development"> </environment> <environment names="Staging,Production"> <link rel="stylesheet" asp-href-include="~/styles*.css" /> </environment> </head> <body> @RenderBody() <app-root></app-root> <environment names="Development"> <script type="text/javascript" src="/inline.bundle.js"></script> <script type="text/javascript" src="/polyfills.bundle.js"></script> <script type="text/javascript" src="/scripts.bundle.js"></script> <script type="text/javascript" src="/styles.bundle.js"></script> <script type="text/javascript" src="/vendor.bundle.js"></script> <script type="text/javascript" src="/main.bundle.js"></script> </environment> <environment names="Production,Staging"> <script type="text/javascript" asp-src-include="~/inline*.js"></script> <script type="text/javascript" asp-src-include="~/polyfills*.js"></script> <script type="text/javascript" asp-src-include="~/scripts*.js"></script> <script type="text/javascript" asp-src-include="~/vendor*.js"></script> <script type="text/javascript" asp-src-include="~/main*.js"></script> </environment> </body> </html>
[name].[hash].bundle.js
همچنین باید دقت داشت که در حالت توسعه، تمام شیوه نامههای برنامه در فایل styles.bundle.js قرار میگیرند. اما در حالت ارائهی نهایی، این فایل وجود نداشته و با نام کلی styles*.css تولید میشود که باید در head صفحه قرار گیرد (مانند تنظیمات حالت تولید در Layout فوق).
اصلاح قسمت URL Rewrite برنامه
در حالت کار با برنامههای تک صفحهای وب، در اولین درخواست رسیدهی به برنامه ممکن است آدرسی درخواست شود که معادل کنترلر و اکشن متدی را در برنامهی سمت سرور نداشته باشد. در این حالت کاربر را به همان صفحهی index.html هدایت میکنیم تا سیستم مسیریابی سمت کلاینت، کار نمایش آن صفحه را انجام دهد:
app.Use(async (context, next) => { await next(); var path = context.Request.Path.Value; if (path != null && context.Response.StatusCode == 404 && !Path.HasExtension(path) && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { context.Request.Path = "/index.html"; await next(); } });
//context.Request.Path = "/index.html"; context.Request.Path = "/"; // since we are using views/shared/_layout.cshtml now.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
- مباحث اعتبارسنجی سمت کلاینت در اینجا بحث شدند.
+ بجای مقدار رشتهای "Null" (که یک مقدار واقعی و غیرنال هست)، از "" استفاده کنید (مقدار خالی). اگر مقدار انتخاب شدهی یک drop down خالی باشد، نال ترجمه میشود. بعد هم از NotEmpty برای بررسی آن استفاده کنید:
RuleFor(x => x.StringField).NotNull().NotEmpty().WithMessage("...");
اگر به دو مطلب استفاده از Quartz.Net (^ و ^) و خصوصا نظرات آن دقت کرده باشید به این نتیجه خواهید رسید که ... این کتابخانهی در اصل جاوایی گنگ طراحی شدهاست. در سایت جاری برای انجام کارهای زمانبندی شده (مانند ارسال ایمیلهای روزانه خلاصه مطالب، تهیه خروجی PDF و XML سایت، تبدیل پیش نویسها به مطالب، بازسازی ایندکسهای جستجو و امثال آن) از یک Thread timer استفاده میشود که حجم نهایی کتابخانهی محصور کننده و مدیریت کنندهی وظایف آن جمعا 8 کیلوبایت است؛ متشکل از ... سه کلاس. در ادامه کدهای کامل و نحوهی استفاده از آن را بررسی خواهیم کرد.
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
برای نمونه :
- در اینجا Order، ترتیب اجرای وظیفهی جاری را در مقایسه با سایر وظیفههایی که قرار است در یک زمان مشخص اجرا شوند، مشخص میکند.
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
- خاصیت IsShuttingDown موجود در کلاس پایه ScheduledTaskTemplate، توسط کتابخانهی DNT Scheduler مقدار دهی میشود. این کتابخانه قادر است زمان خاموش شدن پروسهی فعلی IIS را تشخیص داده و خاصیت IsShuttingDown را true کند. بنابراین در حین اجرای وظیفهای مشخص، به مقدار IsShuttingDown دقت داشته باشید. اگر true شد، یعنی فقط 30 ثانیه وقت دارید تا کار را تمام کنید.
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
- شیء ScheduledTasksCoordinator.Current، نمایانگر تنها وهلهی مدیریت وظایف برنامه است.
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
- متد ScheduledTasksRegistry.Init در حین آغاز برنامه فراخوانی میشود.
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
همینکه درخواست مجددی به این صفحه برسد، مجددا برنامه توسط IIS بارگذاری شده و اجرا میگردد. به این ترتیب وظایف تعریف شده، در طول یک روز بدون مشکل کار خواهند کرد.
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
لیست حاصل را به سادگی میتوان در یک Grid نمایش داد.
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
public class SendEmailsTask : ScheduledTaskTemplate
using System; namespace DNTScheduler.TestWebApplication.WebTasks { public class SendEmailsTask : ScheduledTaskTemplate { /// <summary> /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آنها را مشخص خواهد کرد /// </summary> public override int Order { get { return 1; } } public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Minute % 2 == 0 && now.Second == 1; } public override void Run() { if (this.IsShuttingDown || this.Pause) return; System.Diagnostics.Trace.WriteLine("Running Send Emails"); } public override string Name { get { return "ارسال ایمیل"; } } } }
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Hour == 23 && now.Minute == 33 && now.Second == 1; }
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System; using System.Net; namespace DNTScheduler.TestWebApplication.WebTasks { public static class ScheduledTasksRegistry { public static void Init() { ScheduledTasksCoordinator.Current.AddScheduledTasks( new SendEmailsTask(), new DoBackupTask()); ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) => { //todo: log the exception. System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message); }; ScheduledTasksCoordinator.Current.Start(); } public static void End() { ScheduledTasksCoordinator.Current.Dispose(); } public static void WakeUp(string pageUrl) { try { using (var client = new WebClient()) { client.Credentials = CredentialCache.DefaultNetworkCredentials; client.Headers.Add("User-Agent", "ScheduledTasks 1.0"); client.DownloadData(pageUrl); } } catch (Exception ex) { //todo: log ex System.Diagnostics.Trace.WriteLine(ex.Message); } } } }
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System; using System.Configuration; using DNTScheduler.TestWebApplication.WebTasks; namespace DNTScheduler.TestWebApplication { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { ScheduledTasksRegistry.Init(); } protected void Application_End() { ScheduledTasksRegistry.End(); //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]); } } }
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
<?xml version="1.0"?> <configuration> <appSettings> <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" /> </appSettings> </configuration>
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new { TaskName = x.Name, LastRunTime = x.LastRun, LastRunWasSuccessful = x.IsLastRunSuccessful, IsPaused = x.Pause, }).ToList();
طی این مقاله، نحوهی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامهی هدف ما که از این API استفاده میکند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings) { // example of saving _settings.General.SiteName = "دات نت تیپس"; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; _settings.Save(); }
همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنامهای General و Seo است که شامل تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایهی SettingBase ارث بری کردهاند. دو دلیل برای انجام این کار وجود دارد:
- تنظیمات به صورت گروه بندی شده در کنار هم قرار گرفتهاند و یافتن تنظیمات برای زمانی که نیاز به دسترسی به آنها داریم، راحتتر و سادهتر خواهد بود.
- به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.
اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟
شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و میتوان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامهی وب شما ری استارت خواهد شد (چه زمانهایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس میباشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده میکنم.
مراحل ساخت Setting API مورد نظر به شرح زیر است:
- ساخت یک Asp.net Web Application
- ساخت مدل Setting و افزودن آن به کانتکست Entity Framework
- ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
- ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کردهاند.
- ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات
یک برنامهی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحلهی اول ما به پایان رسید؛ چرا که ویژوال استودیو کارهای مورد نیاز ما را انجام خواهد داد.
لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models { public interface IUnitOfWork { DbSet<Setting> Settings { get; set; } int SaveChanges(); } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork { public DbSet<Setting> Settings { get; set; } public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } } namespace DynamicSettingAPI.Models { public class Setting { public string Name { get; set; } public string Type { get; set; } public string Value { get; set; } } }
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Setting>() .HasKey(x => new { x.Name, x.Type }); modelBuilder.Entity<Setting>() .Property(x => x.Value) .IsOptional(); base.OnModelCreating(modelBuilder); }
ساختاری به شکل زیر مد نظر ماست:
کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service { public abstract class SettingsBase { //1 private readonly string _name; private readonly PropertyInfo[] _properties; protected SettingsBase() { //2 var type = GetType(); _name = type.Name; _properties = type.GetProperties(); } public virtual void Load(IUnitOfWork unitOfWork) { //3 get setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { //get the setting from setting list var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { //4 set propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType)); } } } public virtual void Save(IUnitOfWork unitOfWork) { //5 get all setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { var propertyValue = propertyInfo.GetValue(this, null); var value = (propertyValue == null) ? null : propertyValue.ToString(); var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { // 6 update existing value setting.Value = value; } else { // 7 create new setting var newSetting = new Setting() { Name = propertyInfo.Name, Type = _name, Value = value, }; unitOfWork.Settings.Add(newSetting); } } } } }
متد Load وظیفهی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند.
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفهی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد.
این متد دیتاهای موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.
کلاسهای تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } public bool RegisterUsersEnabled { get; set; } } public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } }
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم:
public interface ISettings { GeneralSettings General { get; } SeoSettings Seo { get; } void Save(); } public class Settings : ISettings { // 1 private readonly Lazy<GeneralSettings> _generalSettings; // 2 public GeneralSettings General { get { return _generalSettings.Value; } } private readonly Lazy<SeoSettings> _seoSettings; public SeoSettings Seo { get { return _seoSettings.Value; } } private readonly IUnitOfWork _unitOfWork; public Settings(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; // 3 _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>); } public void Save() { // only save changes to settings that have been loaded if (_generalSettings.IsValueCreated) _generalSettings.Value.Save(_unitOfWork); if (_seoSettings.IsValueCreated) _seoSettings.Value.Save(_unitOfWork); _unitOfWork.SaveChanges(); } // 4 private T CreateSettings<T>() where T : SettingsBase, new() { var settings = new T(); settings.Load(_unitOfWork); return settings; } }
این اینترفیس مشخص میکند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفهی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.
حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازندهای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache; public Settings(IUnitOfWork unitOfWork, ICache cache) { // ARGUMENT CHECKING SKIPPED FOR BREVITY _unitOfWork = unitOfWork; _cache = cache; _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>); } private T CreateSettingsWithCache<T>() where T : SettingsBase, new() { // this is where you would implement loading from ICache throw new NotImplementedException(); }
public ActionResult Index() { using (var uow = new ApplicationDbContext()) { var _settings = new Settings(uow); _settings.General.SiteName = "دات نت تیپس"; _settings.General.AdminEmail = "admin@gmail.com"; _settings.General.RegisterUsersEnabled = true; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; var settings2 = new Settings(uow); var output = string.Format("SiteName: {0} HomeMetaDescription: {1} MetaKeywords: {2} MetaTitle: {3} RegisterEnable: {4}", settings2.General.SiteName, settings2.Seo.HomeMetaDescription, settings2.Seo.MetaKeywords, settings2.Seo.HomeMetaTitle, settings2.General.RegisterUsersEnabled.ToString() ); return Content(output); } }
نکته: در پروژه ای که جدیدا در سایت ارائه دادهام و در حال تکمیل آن هستم، از بهبود یافتهی این مقاله استفاده میشود. حتی برای اسلاید شوهای سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژهی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفتهتر از این مقاله، همچین امکانی را دارد.
در مطلب «نحوهی مشارکت در پروژههای GitHub به کمک Visual Studio» با مفهوم pull request آشنا شدیم. اما ... یک pull request خوب چه خصوصیاتی دارد و فرهنگ ارسال یک PR خوب چیست؟
اخلاق مشارکت در یک پروژهی سورس باز
بعضی از توسعه دهندهها در حین مشارکت در یک پروژهی سورس باز، برای مثال جهت افزودن قابلیتی جدید و یا رفع مشکلی، ابتدا سعی میکنند تا کدهای فعلی را برای خودشان «قابل فهمتر» کنند. این قابل فهمتر کردن پروژه، شامل تغییر نام متغیرها و متدهای فعلی، انتقال کدهای موجود به فایلهایی دیگر یا حتی یکی کردن چندین فایل با هم، مرتب سازی متدهای یک کلاس بر اساس حروف الفباء و امثال آن میشود.
این کارها را نباید در حین مشارکت و توسعهی پروژههای سورس باز دیگران انجام دهید! اگر هدفتان رفع مشکلی است یا افزودن قابلیتی جدید، باید نحوهی کدنویسی فعلی را حفظ کنید. از این جهت که نگهدارندهی اصلی پروژه، پیش از شما اینکار را شروع کردهاست و زمانیکه شما به پروژهای دیگر رجوع خواهید کرد، باز نیز باید همین کار را ادامه دهد.
اگر refactoring گستردهی شما به هر نحوی سبب بهبود پروژهی اصلی میشود، ابتدا این مورد را با مسئول اصلی پروژه مطرح کنید. اگر او قبول کرد، سپس اقدام به چنین کاری نمائید.
بحث در مورد تغییرات پیش از ارسال PR
قبل از اینکه PR ایی را ارسال کنید، بهتر است یک issue یا ticket جدید را باز کرده و در مورد آن بحث کنید یا توضیح دهید. در این حالت ممکن است توضیحات بهتری را در مورد سازگار سازی تغییرات خود با کدهای فعلی دریافت کنید.
Pull requestها را کوچک نگهدارید
برای اینکه شانس قبول شدن PR خود را بالا ببرید، حجم و تمرکز آنرا کوچک نگه دارید. بسیاری از توسعه دهندههای سورس باز اگر با یک PR حجیم روبرو شوند، آنرا رد میکنند چون مشکل اصلی، مدت زمان بالایی است که باید جهت بررسی این PR اختصاص داد. هرچقدر حجم آن بیشتر باشد، زمان بیشتری را خواهد برد.
فقط یک کار را انجام دهید
شبیه به اصل تک مسئولیتی کلاسها، یک PR نیز باید تنها یک کار را انجام دهد و بر روی یک موضوع خاص تمرکز داشته باشد. فرض کنید PR ایی را ارسال کردهاید که سه مشکل A، B و C را برطرف میکند. از دیدگاه مسئول اصلی پروژه، موارد A و C قابل قبول هستند؛ اما نه مورد C مطرح شده. در این حالت کل PR شما برگشت خواهد خورد. به همین جهت بهتر است بجای یک PR، سه PR مختلف و مجزا را جهت رفع مشکلات A، B و C ارسال کنید.
سازگاری تغییرات ارسالی را بررسی کنید
حداقل کاری را که پیش از ارسال PR باید انجام دهید این است که بررسی کنید آیا این تغییرات قابل Build هستند یا خیر. همچنین اگر پروژه دارای یک سری Unit tests است، حتما آنها را یکبار بررسی کنید تا مطمئن شوید جای دیگری را به هم نریختهاید. ضمنا وجود این تستها به صورت ضمنی به این معنا است که تغییرات جدید شما نیز باید به همراه تستهای مرتبطی باشند تا پذیرفته شوند.
PR ایی را بر روی شاخهی master ارسال نکنید
پس از اینکه یک fork از پروژهای سورس باز را ایجاد کردید و سپس آنرا clone نمودید تا به صورت Local بتوانید با آن کار کنید، فراموش نکنید که در همینجا باید یک branch و انشعاب جدید را جهت کار بر روی ویژگی مدنظر خود ایجاد کنید (برای مثال feature-X, fix-Y). بسیاری از پروژههای سورس باز به هیچ عنوان PRهای کار شدهی بر روی انشعاب master را قبول نمیکنند.
برای مطالعه بیشتر
Open Source Contribution Etiquette
ten tips for better Pull Requests
Getting a Pull Request Accepted
Optimize Your Pull-request
اخلاق مشارکت در یک پروژهی سورس باز
بعضی از توسعه دهندهها در حین مشارکت در یک پروژهی سورس باز، برای مثال جهت افزودن قابلیتی جدید و یا رفع مشکلی، ابتدا سعی میکنند تا کدهای فعلی را برای خودشان «قابل فهمتر» کنند. این قابل فهمتر کردن پروژه، شامل تغییر نام متغیرها و متدهای فعلی، انتقال کدهای موجود به فایلهایی دیگر یا حتی یکی کردن چندین فایل با هم، مرتب سازی متدهای یک کلاس بر اساس حروف الفباء و امثال آن میشود.
این کارها را نباید در حین مشارکت و توسعهی پروژههای سورس باز دیگران انجام دهید! اگر هدفتان رفع مشکلی است یا افزودن قابلیتی جدید، باید نحوهی کدنویسی فعلی را حفظ کنید. از این جهت که نگهدارندهی اصلی پروژه، پیش از شما اینکار را شروع کردهاست و زمانیکه شما به پروژهای دیگر رجوع خواهید کرد، باز نیز باید همین کار را ادامه دهد.
اگر refactoring گستردهی شما به هر نحوی سبب بهبود پروژهی اصلی میشود، ابتدا این مورد را با مسئول اصلی پروژه مطرح کنید. اگر او قبول کرد، سپس اقدام به چنین کاری نمائید.
بحث در مورد تغییرات پیش از ارسال PR
قبل از اینکه PR ایی را ارسال کنید، بهتر است یک issue یا ticket جدید را باز کرده و در مورد آن بحث کنید یا توضیح دهید. در این حالت ممکن است توضیحات بهتری را در مورد سازگار سازی تغییرات خود با کدهای فعلی دریافت کنید.
Pull requestها را کوچک نگهدارید
برای اینکه شانس قبول شدن PR خود را بالا ببرید، حجم و تمرکز آنرا کوچک نگه دارید. بسیاری از توسعه دهندههای سورس باز اگر با یک PR حجیم روبرو شوند، آنرا رد میکنند چون مشکل اصلی، مدت زمان بالایی است که باید جهت بررسی این PR اختصاص داد. هرچقدر حجم آن بیشتر باشد، زمان بیشتری را خواهد برد.
فقط یک کار را انجام دهید
شبیه به اصل تک مسئولیتی کلاسها، یک PR نیز باید تنها یک کار را انجام دهد و بر روی یک موضوع خاص تمرکز داشته باشد. فرض کنید PR ایی را ارسال کردهاید که سه مشکل A، B و C را برطرف میکند. از دیدگاه مسئول اصلی پروژه، موارد A و C قابل قبول هستند؛ اما نه مورد C مطرح شده. در این حالت کل PR شما برگشت خواهد خورد. به همین جهت بهتر است بجای یک PR، سه PR مختلف و مجزا را جهت رفع مشکلات A، B و C ارسال کنید.
سازگاری تغییرات ارسالی را بررسی کنید
حداقل کاری را که پیش از ارسال PR باید انجام دهید این است که بررسی کنید آیا این تغییرات قابل Build هستند یا خیر. همچنین اگر پروژه دارای یک سری Unit tests است، حتما آنها را یکبار بررسی کنید تا مطمئن شوید جای دیگری را به هم نریختهاید. ضمنا وجود این تستها به صورت ضمنی به این معنا است که تغییرات جدید شما نیز باید به همراه تستهای مرتبطی باشند تا پذیرفته شوند.
PR ایی را بر روی شاخهی master ارسال نکنید
پس از اینکه یک fork از پروژهای سورس باز را ایجاد کردید و سپس آنرا clone نمودید تا به صورت Local بتوانید با آن کار کنید، فراموش نکنید که در همینجا باید یک branch و انشعاب جدید را جهت کار بر روی ویژگی مدنظر خود ایجاد کنید (برای مثال feature-X, fix-Y). بسیاری از پروژههای سورس باز به هیچ عنوان PRهای کار شدهی بر روی انشعاب master را قبول نمیکنند.
برای مطالعه بیشتر
Open Source Contribution Etiquette
ten tips for better Pull Requests
Getting a Pull Request Accepted
Optimize Your Pull-request
مطالب دورهها
تزریق وابستگیها و سناریوهای بسیار متعدد موجود
تعدادی از پرکاربردترین حالتهای تزریق وابستگیها را در دوره جاری بررسی کردیم. برای مثال چگونه میتوان تزریق وابستگیهای یک کنترلر ASP.NET MVC را خودکار کرد، یا در وب فرمها وضعیت چگونه است. اما در حین حل مسایلی از این دست، به سناریوهای بسیار متعددی برخواهید خورد. برای مثال تزریق وابستگیهای خودکار در یک کنترلر را آموختیم؛ در مورد فیلترها و Action Resultهای سفارشی چطور باید رفتار کرد؟ در WCF چطور؟ در هندلرهای وب فرمها چطور؟ و بسیاری از حالات دیگر. البته تمام این موارد را توسط الگوی Service locator که شامل استفاده مستقیم از امکانات وهله سازی یک IoC Container، در کلاس مدنظر است، میتوان حل کرد؛ اما باید تا حد امکان از این روش با توجه به اینکه خود IoC Container را تبدیل به یک وابستگی مدفون شده در کلاسهای ما میکند، پرهیز نمود.
اگر به دنبال کتابخانهای هستید که بسیاری از این سناریوها را پیاده سازی کرده است، کتابخانه AutoFac پیشنهاد میشود. حتی اگر علاقمند به استفاده از آن نباشید، میتوان از نحوه پیاده سازیهای مختلف آن در مورد حالتهای مختلف خودکار سازی تزریق وابستگیها، ایده گرفت و سپس این کدها را با IoC Container مورد علاقه خود پیاده سازی کرد.
صفحه خانگی AutoFac
http://code.google.com/p/autofac
http://autofac.org
بسته نیوگت
http://www.nuget.org/packages/Autofac
محلی برای ایده گرفتن مثلا در مورد فیلترهای ASP.NET MVC
و در مورد نحوه استفاده از آنها، نیاز است آزمونهای واحد این پروژه را بررسی کنید و یا مستندات پروژه را مطالعه کنید.
همچنین بررسی لیست مستندات کلی آن نیز بسیار مفید است
به صورت خلاصه، هرجایی در مورد تزریق وابستگیهای خودکار جهت پرهیز از استفاده مستقیم از الگوی Service locator ایدهای نداشتید، سورس پروژه AutoFac را بررسی کنید.
پ.ن.
سایت bitbucket امکان import کامل مخازن کد Google code را نیز دارد (در صورتیکه دسترسی شما به گوگل کد محدود است).
اگر به دنبال کتابخانهای هستید که بسیاری از این سناریوها را پیاده سازی کرده است، کتابخانه AutoFac پیشنهاد میشود. حتی اگر علاقمند به استفاده از آن نباشید، میتوان از نحوه پیاده سازیهای مختلف آن در مورد حالتهای مختلف خودکار سازی تزریق وابستگیها، ایده گرفت و سپس این کدها را با IoC Container مورد علاقه خود پیاده سازی کرد.
صفحه خانگی AutoFac
http://code.google.com/p/autofac
http://autofac.org
بسته نیوگت
http://www.nuget.org/packages/Autofac
محلی برای ایده گرفتن مثلا در مورد فیلترهای ASP.NET MVC
و در مورد نحوه استفاده از آنها، نیاز است آزمونهای واحد این پروژه را بررسی کنید و یا مستندات پروژه را مطالعه کنید.
همچنین بررسی لیست مستندات کلی آن نیز بسیار مفید است
به صورت خلاصه، هرجایی در مورد تزریق وابستگیهای خودکار جهت پرهیز از استفاده مستقیم از الگوی Service locator ایدهای نداشتید، سورس پروژه AutoFac را بررسی کنید.
پ.ن.
سایت bitbucket امکان import کامل مخازن کد Google code را نیز دارد (در صورتیکه دسترسی شما به گوگل کد محدود است).
- در نمونه بحث جاری کنترل بیشتری بر روی رویدادها خواهید داشت. مثلا قسمت xhr.status == 403 و هدایت کاربر به صفحه لاگین در صورت منقضی شدن اعتبارسنجی آن.
با توجه به اینکه گفتید فرم ایجکس داره همین روال رو در پشت صحنه ایجاد میکنه برای هدایت کاربر به صفحه لاگین نمیشه کد مربوطه رو تو OnComplete شی AjaxOptions نوشت؟
- Ajax.BeginForm برای کار کردن حتما نیاز به submit button داره. در مطلب جاری از یک span هم میتونید استفاده کنید و مشکلی نداره.
آیا نمیشه این سابمیت رو با جاوااسکریپت پیاده سازی کرد؟که بشه تو رویداد کلیک هر المنتی نوشت؟
- در Ajax.BeginForm آنچنان کنترلی بر روی پردازش نهایی خروجی اکشن متد ندارید. مثلا در اینجا عنوان شد که اگر خروجی JSON بود و اگر دارای فیلد مشخصی با مقدار مشخصی بود نیاز است کار خاصی انجام شود. در حالت jQuery Ajax مستقیم، پردازش JSON سادهتر است.
اینم نمیشه نتیجه نهایی رو تو OnComplete شی AjaxOptions داشته باشیم؟
سلام ، اگه بخوایم مقدار ورودیهای اکشنمون به صورت داینامیک باشه چیکار باید کنیم؟
چنین مدلی دارم که میخوام دکمهی حذفش به صورت Jquery Ajax باشه
ولی مقدار id رو باید به اکشن بفرستم ! اونم تو یه حلقه Foreach . باید کدهای جی کوئری تو حلقه باشه؟ به نظرتون راه حل چیه؟ ممنون
منظورم به عنوان مثال مقادیر این خط در مثال شماست
var postId = 10; var fileName = "test.jpg"
@foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Title) </td> <td> @Html.DisplayFor(modelItem => item.IsRead) </td> <td> @Html.ActionLink("حذف پیام", MVC.User.ActionNames.DeleteMessageSend, MVC.User.Name, new{ id=item.Id }, new { @class="btn btn-danger" }) </td> </tr> }