اگر دقت کرده باشید در کنار هر لینک خارجی (هر لینک ختم شده به خارج از سایت) در این وبلاگ، آیکون آن سایت هم به صورت خودکار نمایش داده میشود. از نظر من جالب بوده (خصوصا جهت مشخص کردن وجود یک لینک در میان انبوهی متن، یا برای نمایش لیستی از لینکها)، نظر شما رو نمیدونم!
اینکار در ادامهی مطلبی است که در مورد نوشتن اسکریپتهای گریس مانکی چندی قبل منتشر کردم. البته بهبود یافتهی آن است. برای ایجاد یک چنین قابلیتی در سایت فقط کافی است چند سطر اسکریپت زیر را به هدر سایت خود اضافه کنید.
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js' type='text/javascript' />
<script type="text/javascript">
$(document).ready(function(){
$("a").each(function(){
var $a = $(this);
var href = $a.attr("href");
// see if the link is external
if (href && href.match(/^http/))
if (!href.match(document.domain)) {
var domain = href.replace(/<\S[^><]*>/g, "").split('/')[2];
var image = '<img src="http://' + domain +
'/favicon.ico" width="0" ' +
' onload="this.width=16;this.height=16;this.style.paddingLeft=\'3px\';this.style.paddingRight=\'1px\';" ' +
' style="border:0" ' +
' onerror="this.src=\'http://vahid.nasiri.googlepages.com/weblink.gif\';" />';
$(this).prepend(image);
}
});
});
</script>
با استفاده از jQuery ، زمانیکه document ما ready شد ( document.ready )، تمام لینکهای موجود در صفحه را پیدا میکنیم (یک به یک). سپس مقدار href آنها را میخوانیم (ببینید با jQuery انجام این نوع کارها چقدر ساده شده است). اکنون روی لینک دریافت شده باید پردازش صورت گیرد و نام دومین آن جدا شود (اگر ختم به خارج از سایت بود). سپس بر اساس این دومین ، یک تگ img منتهی به آیکون آن سایت طراحی و نمایش داده خواهد شد (به قبل از لینک اضافه میشود).
عموما هر پروژهای هر چند کوچک و به ظاهر کم اهمیت، نکات خاصی را به همراه دارد.
برای مثال، در ابتدای کار width=0 در نظر گرفته نشد. مشکلی را که ایجاد کرد این بود که یک سایت ممکن است اصلا favicon.ico نداشته باشد. فایرفاکس محترم اصلا به روی مبارک هم نخواهد آورد و شما متوجه نخواهید شد که در صفحه کمبود تصویری وجود دارد. اما در IE حتما جای خالی آیکون و تصویر به نحوه واضحی گوشزد میشود. بنابراین در ابتدای کار قرار نیست چیزی را نمایش دهیم (width=0). سپس در رخداد onload تگ img، اگر تصویر واقعا وجود داشت و بارگذاری شد، طول و عرض آنرا 16 در 16 قرار خواهیم داد (این مورد هم لازم است. چون بعضی از سایتها اندازههای بسیار بزرگی را برای آیکون خود در نظر میگیرند که اصلا مقصود ما را برآورده نخواهند کرد).
این مورد (عدم نمایش تصاویری که وجود ندارند) مشکلی را که در ادامه ایجاد خواهد کرد، عدم یکنواختی در سایت است. یک سری از لینکهای خارجی آیکون دارند و یک سری خیر. نکته جالبی که در مورد تگ img وجود دارد رخداد onerror آن است. اگر مرورگر نتواند تصویر مورد نظر را پیدا یا بارگذاری کند، این روال رخداد گردان فراخوانی میشود. همینجا از موقعیت استفاده شده و src تصویر جاری به یک آیکون مشخص و از قبل تعیین شده تنظیم میشود.
برای ساخت یک extender کافیست تابع مورد نظر را به عنوان آرگومان به شی ko.extenders پاس دهیم. پارامتر اول این تابع، شیء observable شده مورد نظر و پارامتر دوم آن، شیء option برای انجام یک سری تنظیمات یا فرستادن مقادیر مورد نظر به تابع است. خروجی این تابع نیز میتواند یک شی observable یا حتی یک شی computed/observable نیز باشد.
یک مثال ساده برای extenderها به صورت زیر است:
ko.extenders.logOpt = function(target, option) { target.subscribe(function(newValue) { console.log(option + ": " + newValue); }); return target; };
برای استفاده از این extender کافیست آن را در هنگام تعریف تابع observable برای خواص، به صورت زیر فراخوانی نمایید:
this.firstName = ko.observable("Masoud").extend({logOpt: "my first name"});
my first name : Masoud
پیاده سازی یک extender جهت اعلام هشدار برای مقادیر منفی
برای اینکه هنگام ورود دادهها توسط کاربر، بتوانیم با ورود مقادیر منفی یک هشدار (تغییر رنگ ورودی) اعلام کنیم، میتوان به صورت زیر عمل نمود:
ko.extenders.negativeValueWarn = function (target, option) { target.hasWarning = ko.observable(); function warn(newValue) { if(newValue && newValue.substring) { newValue = parseFloat(newValue); } target.hasWarning(newValue < 0 ? true : false); } warn(target()); target.subscribe(warn); return target; };
یاد آوری : در KO برای انتساب مقدار جدید به خواصی که به صورت observable تعریف شده اند به صورت زیر:
target(NewValue) => فراخوانی به صورت تابع و پاس دادن مقدار جدید به آن
target() => فراخوانی به صورت تابع بدون آرگومان
پیاده سازی یک extender برای انتساب مقادیر Boolean به Radio Button ها
برای اینکه radio buttonها نیز بتوانند فقط با مقادیر Boolean مقدار دهی شوند و از طرفی در هنگام عملیات مقید سازی و ارسال نتایج در قالب شی Json به سرور، بدون هیچ گونه تغییر و محاسبات مقادیر مورد نظر به صورت true/false (از نوع Boolean) باشند میتوان به صورت زیر عمل نمود:
ko.extenders["booleanValue"] = function (target) { target.formattedValue = ko.computed({ read: function () { if (target() === true) return "True"; else if (target() === false) return "False"; }, write: function (newValue) { if (newValue) { if (newValue === "False") target(false); else if (newValue === "True") target(true); } } }); target.formattedValue(target()); return target; };
با فرض اینکه کدهای Html صفحه به صورت زیر است:
<span>Do you want fries with that?</span> <label> <input type="radio" name="question" value="True" data-bind="value: myValue.formattedValue" /> Yes </label> <label> <input type="radio" name="question" value="False" data-bind="value: myValue.formattedValue" /> No </label>
برای استفاده از آن کافیست به صورت زیر عمل نمایید:
this.myValue= ko.observable(false).extend({ booleanValue: null });
- «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid»
Kendo UI Grid دارای امکانات ثبت، ویرایش و حذف توکاری است که در ادامه نحوهی فعال سازی آنها را بررسی خواهیم کرد. مثالی که در ادامه بررسی خواهد شد، در تکمیل مطلب «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid» است.
تنظیمات Data Source سمت کاربر
برای فعال سازی صفحه بندی سمت سرور، با قسمت read منبع داده Kendo UI پیشتر آشنا شده بودیم. جهت فعال سازی قسمتهای ثبت اطلاعات جدید (create)، به روز رسانی رکوردهای موجود (update) و حذف ردیفی مشخص (destroy) نیاز است تعاریف قسمتهای متناظر را که هر کدام به آدرس مشخصی در سمت سرور اشاره میکنند، اضافه کنیم:
var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "api/products", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (product) { return "api/products/" + product.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (product) { return "api/products/" + product.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, //... }, schema: { //... model: { id: "Id", // define the model of the data source. Required for validation and property types. fields: { "Id": { type: "number", editable: false }, //تعیین نوع فیلد برای جستجوی پویا مهم است "Name": { type: "string", validation: { required: true } }, "IsAvailable": { type: "boolean" }, "Price": { type: "number", validation: { required: true, min: 1 } }, "AddDate": { type: "date", validation: { required: true } } } } }, batch: false, // enable batch editing - changes will be saved when the user clicks the "Save changes" button //... });
- به علاوه در اینجا به ازای هر فیلد، مباحث اعتبارسنجی نیز اضافه شدهاند؛ برای مثال فیلدهای اجباری با required: true مشخص گردیدهاند.
- اگر فیلدی نباید ویرایش شود (مانند فیلد Id)، خاصیت editable آنرا false کنید.
- در data source امکان تعریف خاصیتی به نام batch نیز وجود دارد. حالت پیش فرض آن false است. به این معنا که در حالت ویرایش، تغییرات هر ردیفی، یک درخواست مجزا را به سمت سرور سبب خواهد شد. اگر آنرا true کنید، تغییرات تمام ردیفها در طی یک درخواست به سمت سرور ارسال میشوند. در این حالت باید به خاطر داشت که پارامترهای سمت سرور، از حالت یک شیء مشخص باید به لیستی از آنها تغییر یابند.
مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات
در حالت ثبت، متد Post، توسط آدرس مشخص شده در قسمت create منبع داده گرید، فراخوانی میگردد:
namespace KendoUI06.Controllers { public class ProductsController : ApiController { public HttpResponseMessage Post(Product product) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); var id = 1; var lastItem = ProductDataSource.LatestProducts.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } product.Id = id; ProductDataSource.LatestProducts.Add(product); var response = Request.CreateResponse(HttpStatusCode.Created, product); response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = product.Id })); // گرید آی دی جدید را به این صورت دریافت میکند response.Content = new ObjectContent<DataSourceResult>( new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter()); return response; } } }
var productsDataSource = new kendo.data.DataSource({ //... schema: { data: "Data", total: "Total", } //... });
namespace KendoUI06.Controllers { public class ProductsController : ApiController { public DataSourceResult Get(HttpRequestMessage requestMessage) { var request = JsonConvert.DeserializeObject<DataSourceRequest>( requestMessage.RequestUri.ParseQueryString().GetKey(0) ); var list = ProductDataSource.LatestProducts; return list.AsQueryable() .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter); } } }
response.Content = new ObjectContent<DataSourceResult>( new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter());
متدهای حذف و به روز رسانی سمت سرور نیز چنین امضایی را خواهند داشت:
namespace KendoUI06.Controllers { public class ProductsController : ApiController { public HttpResponseMessage Delete(int id) { var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); ProductDataSource.LatestProducts.Remove(item); return Request.CreateResponse(HttpStatusCode.OK, item); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public HttpResponseMessage Update(int id, Product product) { var item = ProductDataSource.LatestProducts .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); if (!ModelState.IsValid || id != product.Id) return Request.CreateResponse(HttpStatusCode.BadRequest); ProductDataSource.LatestProducts[item.Index] = product; return Request.CreateResponse(HttpStatusCode.OK); } } }
تنظیمات Kendo UI Grid جهت فعال سازی CRUD
در ادامه کلیه تغییرات مورد نیاز جهت فعال سازی CRUD را در Kendo UI، به همراه مباحث بومی سازی عبارات متناظر با دکمهها و صفحات خودکار مرتبط، مشاهده میکنید:
$("#report-grid").kendoGrid({ //.... editable: { confirmation: "آیا مایل به حذف ردیف انتخابی هستید؟", destroy: true, // whether or not to delete item when button is clicked mode: "popup", // options are "incell", "inline", and "popup" //template: kendo.template($("#popupEditorTemplate").html()), // template to use for pop-up editing update: true, // switch item to edit mode when clicked? window: { title: "مشخصات محصول" // Localization for Edit in the popup window } }, columns: [ //.... { command: [ { name: "edit", text: "ویرایش" }, { name: "destroy", text: "حذف" } ], title: " ", width: "160px" } ], toolbar: [ { name: "create", text: "افزودن ردیف جدید" }, { name: "save", text: "ذخیرهی تمامی تغییرات" }, { name: "cancel", text: "لغو کلیهی تغییرات" }, { template: kendo.template($("#toolbarTemplate").html()) } ], messages: { editable: { cancelDelete: "لغو", confirmation: "آیا مایل به حذف این رکورد هستید؟", confirmDelete: "حذف" }, commands: { create: "افزودن ردیف جدید", cancel: "لغو کلیهی تغییرات", save: "ذخیرهی تمامی تغییرات", destroy: "حذف", edit: "ویرایش", update: "ثبت", canceledit: "لغو" } } });
- برای فعال سازی تولید صفحات خودکار ویرایش و افزودن ردیفها، نیاز است خاصیت editable را به نحوی که ملاحظه میکنید، مقدار دهی کرد. خاصیت mode آن سه حالت incell (پیش فرض)، inline و popup را پشتیبانی میکند.
- اگر حالتهای inline و یا popup را فعال کردید، در انتهای ستونهای تعریف شده، نیاز است ستون ویژهای به نام command را مطابق تعاریف فوق، تعریف کنید. در این حالت دو دکمهی ویرایش و ثبت، فعال میشوند و اطلاعات خود را از تنظیمات data source گرید دریافت میکنند. دکمهی ویرایش در حالت incell کاربردی ندارد (چون در این حالت کاربر با کلیک درون یک سلول میتواند آنرا مانند برنامهی اکسل ویرایش کند). اما دکمهی حذف در هر سه حالت قابل استفاده است.
- به نوار ابزار گرید، سه دکمهی افزودن ردیفهای جدید، ذخیرهی تمامی تغییرات و لغو تغییرات صورت گرفته، اضافه شدهاند. این دکمهها استاندارد بوده و در اینجا نحوهی بومی سازی پیامهای مرتبط را نیز مشاهده میکنید. همانطور که عنوان شد، دکمههای «تمامی تغییرات» در حالت فعال سازی batching در منبع داده و استفاده از incell editing معنا پیدا میکند. در سایر حالات این دو دکمه کاربردی ندارند. اما دکمهی افزودن ردیفهای جدید در هر سه حالت کاربرد دارد و یکسان است.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
KendoUI06.zip
معرفی قالبهای جدید شروع پروژههای Blazor در دات نت 8
پس از نصب SDK دات نت 8، دیگر خبری از قالبهای قدیمی پروژههای blazor server و blazor wasm نیست! در اینجا در ابتدا باید مشخص کرد که سطح تعاملی برنامه در چه حدی است. در ادامه 4 روش شروع پروژههای Blazor 8x را مشاهده میکنید که توسط پرچم interactivity--، نوع رندر برنامه در آنها مشخص شدهاست:
اجرای قسمتهای تعاملی برنامه بر روی سرور:
dotnet new blazor --interactivity Server
اجرای قسمتهای تعاملی برنامه در مرورگر، توسط فناوری وباسمبلی:
dotnet new blazor --interactivity WebAssembly
برای اجرای قسمتهای تعاملی برنامه، ابتدا حالت Server فعالسازی میشود تا فایلهای WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده میکند:
dotnet new blazor --interactivity Auto
فقط از حالت SSR یا همان static server rendering استفاده میشود (این نوع برنامهها تعاملی نیستند):
dotnet new blazor --interactivity None
سایر گزینهها را با اجرای دستور dotnet new blazor --help میتوانید مشاهده کنید.
نکتهی مهم! در قالبهای آمادهی Blazor 8x، حالت SSR، پیشفرض است.
هرچند در تمام پروژههای فوق، انتخاب حالتهای مختلف رندر را مشاهده میکنید، اما این انتخابها صرفا دو مقصود مهم را دنبال میکنند:
الف) تنظیم فایل Program.cs برنامه جهت افزودن وابستگیهای مورد نیاز، به صورت خودکار.
ب) ایجاد پروژهی کلاینت (علاوه بر پروژهی سرور)، در صورت نیاز. برای مثال حالتهای وباسمبلی و Auto، هر دو به همراه یک پروژهی کلاینت وباسمبلی هم هستند؛ اما حالتهای Server و None، خیر.
در تمام این پروژهها هر صفحه و یا کامپوننتی که ایجاد میشود، به صورت پیشفرض بر اساس SSR رندر و نمایش داده خواهد شد؛ مگر اینکه به صورت صریحی این نحوهی رندر را بازنویسی کنیم. برای مثال مشخص کنیم که قرار است بر اساس Blazor Server اجرا شود و یا وباسمبلی و یا حالت Auto.
بررسی حالت Server side rendering
برای بررسی این حالت یک پوشهی جدید را ایجاد کرده و توسط خط فرمان، دستور dotnet new blazor --interactivity Server را در ریشهی آن اجرا میکنیم. پس از ایجاد ساختار ابتدایی پروژه بر اساس این قالب انتخابی، فایل Program.cs جدید آن، چنین شکلی را دارد:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAntiforgery(); app.MapRazorComponents<App>().AddInteractiveServerRenderMode(); app.Run();
server-side rendering به این معنا است که برنامهی سمت سرور، کل DOM و HTML نهایی را تولید کرده و به مرورگر کاربر ارائه میکند. مرورگر هم این DOM را نمایش میدهد. فقط همین! در اینجا هیچ خبری از اتصال دائم SignalR نیست و محتوای ارائه شده، یک محتوای استاتیک است. این حالت رندر، برای ارائهی محتواهای فقط خواندنی غیرتعاملی، فوق العادهاست؛ امکان از لحظهای که نیاز به کلیک بر روی دکمهای باشد، دیگر پاسخگو نیست. به همین جهت در اینجا امکان تعاملی کردن تعدادی از کامپوننتهای ویژه و مدنظر نیز پیشبینی شدهاند تا بتوان به ترکیبی از server-side rendering و client-side rendering رسید.
حالت پیشفرض در اینجا، ارائهی محتوای استاتیک است. بنابراین هر کامپوننتی در اینجا ابتدا بر روی سرور رندر شده (HTML ابتدایی آن آماده شده) و به سمت مرورگر کاربر ارسال میشود. اگر کامپوننتی نیاز به امکانات تعاملی داشت باید آنرا دقیقا توسط ویژگی InteractiveXYZ مشخص کند؛ مانند مثال زیر:
@page "/counter" @rendermode InteractiveServer <PageTitle>Counter</PageTitle> <h1>Counter</h1> <p role="status">Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
در ادامه، مجددا سطر کامنت شده را به حالت عادی برگردانید و سپس برنامه را اجرا کنید. پیش از باز کردن صفحهی Counter، ابتدا developer tools مرورگر خود را گشوده و برگهی network آنرا انتخاب و سپس صفحهی Counter را باز کنید. در این لحظهاست که مشاهده میکنید یک اتصال وبسوکت برقرار شد. این اتصال است که قابلیتهای تعاملی صفحه را برقرار کرده و مدیریت میکند (این اتصال دائم SignalR است که این صفحه را همانند برنامههای Blazor Web Server پیشین مدیریت میکند).
یک نکته: در برنامههای Blazor Server سنتی، امکان فعالسازی قابلیتی به نام prerender نیز وجود دارد. یعنی سرور، ابتدا صفحه را رندر کرده و محتوای استاتیک آنرا به سمت مرورگر کاربر ارسال میکند و سپس اتصال SignalR برقرار میشود. در دات نت 8، این حالت، حالت پیشفرض است. اگر آنرا نمیخواهید باید به نحو زیر غیرفعالش کنید:
@rendermode InteractiveServerRenderModeWithoutPrerendering @code{ static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = new InteractiveServerRenderMode(false); }
روشی ساده برای تعاملی کردن کل برنامه
اگر میخواهید رفتار برنامه را همانند Blazor Server سابق کنید و نمیخواهید به ازای هر کامپوننت، نحوهی رندر آنرا به صورت سفارشی انتخاب کنید، فقط کافی است فایل App.razor را باز کرده:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <base href="/" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="MyApp.styles.css" /> <link rel="icon" type="image/png" href="favicon.png" /> <HeadOutlet /> </head> <body> <Routes /> <script src="_framework/blazor.web.js"></script> </body> </html>
<HeadOutlet @rendermode="@InteractiveServer" /> ... <Routes @rendermode="@InteractiveServer" />
در این حالت دیگر نیازی نیست تا به ازای هر کامپوننت و صفحه، نحوهی رندر را مشخص کنیم؛ چون این نحوه، از بالاترین سطح، به تمام زیرکامپوننتها به ارث میرسد (دربارهی این نکته در قسمت قبل، توضیحاتی ارائه شد).
بررسی حالت Streaming Rendering
در اینجا مثال پیشفرض Weather.razor قالب پیشفرض مورد استفادهی جاری را کمی تغییر دادهایم که کدهای نهایی آن به صورت زیر است (2 قسمت forecasts.AddRange_ را اضافهتر دارد):
@page "/weather" @attribute [StreamRendering(prerender: true)] <PageTitle>Weather</PageTitle> <h1>Weather</h1> <p>This component demonstrates showing data.</p> @if (_forecasts == null) { <p> <em>Loading...</em> </p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in _forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private List<WeatherForecast>? _forecasts; protected override async Task OnInitializedAsync() { // Simulate asynchronous loading to demonstrate streaming rendering await Task.Delay(500); var startDate = DateOnly.FromDateTime(DateTime.Now); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching", }; _forecasts = GetWeatherForecasts(startDate, summaries).ToList(); StateHasChanged(); // Simulate asynchronous loading to demonstrate streaming rendering await Task.Delay(1000); _forecasts.AddRange(GetWeatherForecasts(startDate, summaries)); StateHasChanged(); await Task.Delay(1000); _forecasts.AddRange(GetWeatherForecasts(startDate, summaries)); } private static IEnumerable<WeatherForecast> GetWeatherForecasts(DateOnly startDate, string[] summaries) { return Enumerable.Range(1, 5) .Select(index => new WeatherForecast { Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)], }); } private class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } }
در ادامه مجددا سطر ویژگی StreamRendering را به حالت قبلی برگردانید و برنامه را اجرا کنید. در این حالت ابتدا قسمت loading ظاهر میشود و سپس در طی چند مرحله با توجه به Task.Delayهای قرار داده شده، صفحه رندر شده و تکمیل میشود.
اتفاقی که در اینجا رخ میدهد، استفاده از فناوری HTML Streaming است که مختص به مایکروسافت هم نیست. در حالت Streaming، هربار قطعهای از HTML ای که قرار است به کاربر ارائه شود، به صورت جریانی به سمت مرورگر ارسال میشود و مرورگر این قطعهی جدید را بلافاصله نمایش میدهد. نکتهی جالب این روش، عدم نیاز به اتصال SignalR و یا اجرای WASM درون مرورگر است.
Streaming rendering حالت بینابین رندر کامل در سمت سرور و رندر کامل در سمت کلاینت است. در حالت رندر سمت سرور، کل HTML صفحه ابتدا توسط سرور تهیه و بازگشت داده میشود و کاربر باید تا پایان عملیات تهیهی این HTML نهایی، منتظر باقی بماند و در این بین چیزی را مشاهده نخواهد کرد. در حالت Streaming rendering، هنوز هم همان حالت تهیهی HTML استاتیک سمت سرور برقرار است؛ به همراه تعدادی محل جایگذاری اطلاعات جدید. به محض پایان یک عمل async سمت سرور که سه نمونهی آن را در مثال فوق مشاهده میکنید، برنامه، جریان قطعهای از اطلاعات استاتیک را به سمت مرورگر کاربر ارسال میکند تا در مکانهایی از پیش تعیین شده، درج شوند.
در حالت SSR، فقط یکبار شانس ارسال کل اطلاعات به سمت مرورگر کاربر وجود دارد؛ اما در حالت Streaming rendering، ابتدا میتوان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آنرا به محض آماده شدن در طی چند مرحله بازگشت داد. در این حالت نمایش گزارشی از اطلاعاتی که ممکن است با تاخیر در سمت سرور تهیه شوند، سادهتر میشود. یعنی میتوان هربار قسمتی را که تهیه شده، برای نمایش بازگشت داد و کاربر تا مدت زیادی منتظر نمایش کل صفحه باقی نخواهد ماند.
روش نهایی معرفی نحوهی رندر صفحات
بجای استفاده از ویژگیهای RenderModeXyz جهت معرفی نحوهی رندر کامپوننتها و صفحات (که تا پیش از نگارش RTM معرفی شده بودند و چندبار هم تغییر کردند)، میتوان از دایرکتیو جدیدی به نام rendermode@ با سه مقدار InteractiveServer، InteractiveWebAssembly و InteractiveAuto استفاده کرد. برای سهولت تعریف این موارد باید سطر ذیل را به فایل Imports.razor_ اضافه نمود:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@attribute [RenderModeInteractiveServer]
@rendermode InteractiveServer
اگر هم قصد سفارشی سازی آنها را دارید، برای مثال میخواهید prerender را در آنها false کنید، روش کار به صورت زیر است:
@rendermode renderMode @code { static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false); }
چون من همین کار را کردم اما در خروجی که میآورد همه صفحات را میآورد یعنی Buissines object مشتریان را پر میکنم و بهش آدرس فایل را میدهم همه اطلاعات را میآورد و صفحه Product Page را هم نمایش میدهد من میخواهم در یکجا فقط مثلا لیست مشتریان را به صورت خروجی به کاربر نمایش بدهد و در جای دیگر فقط اطلاعات (صفحه) محصولات را به کاربر نمایش بدهد و این کار در فایلهای جدا امکان پذیر است؟ حالا سوالی که دارم این است آیا میشه در یک فایل چنین کاری کرد یا خیر؟
اگر جواب بله است کد C# آن به چه صورت نوشته میشود.
try { var mainReport = new StiReport(); var details = new List<Details>(); foreach (var item in Items) { details.Add(new Details { CarName = item.CarName, }); } mainReport.Load(CurrentDirectory() + @"\Rp.dll"); mainReport.RegBusinessObject("Print", details); mainReport.Show(); } catch (Exception ex) { MessageUtility.ErrorAlert(ex.GetOriginalException().Message); }
- مطالعه مسیر آموزشی "Entity Framework Code-First"
- مطالعه مسیر آموزشی "Asp.NET MVC"
- مطالعه مقالات مربوط به "Asp.net Identity"
- مطالعه مسیر آموزشی "اصول طراحی شی گرا SOLID" و دوره "بررسی مفاهیم معکوس سازی وابستگیها و ابزارهای مرتبط با آن"
- انجمن
- ارتباط دوستی
- سیستم ترفیع رتبه
- Themeable
- سیستم Following
- صفحات داینامیک
- سیستم پیام رسانی
- امکان ساخت گروههای شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی مختلف
- پیغام خصوصی
- وبلاگ
- نظرسنجی ها
- مدیریت کاربران با دسترسیها داینامیک
- اخبار
- آگهی ها
/// <summary> /// Represents the lable /// </summary> public class Tag { #region Ctor /// <summary> /// Create one instance of <see cref="Tag"/> /// </summary> public Tag() { Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// sets or gets Tag's identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// sets or gets Tag's name /// </summary> public virtual string Name { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets Tag's posts /// </summary> public virtual ICollection<BlogPost> BlogPosts { get; set; } #endregion }
/// <summary> /// Represents the Post's Draft /// </summary> public class BlogDraft { #region Ctor /// <summary> /// create one instance of <see cref="BlogDraft"/> /// </summary> public BlogDraft() { Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets Id of post's draft /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets body of post's draft /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or set title of post's draft /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets tags of post's draft that seperated using ',' /// </summary> public virtual string TagNames { get; set; } /// <summary> /// gets or sets value indicating whether this draft is ready to publish /// </summary> public virtual bool IsReadyForPublish { get; set; } /// <summary> /// ges ro sets DateTime that this draft added /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets information of User-Agent /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets date that this draft publish as ready /// </summary> public virtual DateTime? ReadyForPublishOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Id of user that he is owner of this draft /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// gets or sets user that he is owner of this draft /// </summary> public virtual User Owner { get; set; } #endregion }
/// <summary> /// Section of Rating /// </summary> public enum RatingSection { News, Announcement, ForumTopic, BlogComment, NewsComment, PollComment, AnnouncementComment, ForumPost, ... } /// <summary> /// Represents Rating Record regard by section type for Rating System /// </summary> public class UserRating { #region Ctor /// <summary> /// Create one instance of <see cref="UserRating"/> /// </summary> public UserRating() { Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets Id of Rating Record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets value of rate /// </summary> public virtual double RatingValue { get; set; } /// <summary> /// gets or sets Section's Id /// </summary> public virtual long SectionId { get; set; } /// <summary> /// gets or sets Section /// </summary> public virtual RatingSection Section { get; set; } #endregion #region Navigation Properties /// <summary> /// gets or sets user that rate one section /// </summary> public virtual User Rater { get; set; } /// <summary> /// gets or sets Rater Id that rate one section /// </summary> public virtual long RaterId { get; set; } #endregion }
/// <summary> /// Represent the rating as ComplexType /// </summary> [ComplexType] public class Rating { /// <summary> /// sets or gets total of rating /// </summary> public virtual double? TotalRating { get; set; } /// <summary> /// sets or gets rater's count /// </summary> public virtual long? RatersCount { get; set; } /// <summary> /// sets or gets average of rating /// </summary> public virtual double? AverageRating { get; set; } }
/// <summary> /// Represents a base class for every content in system /// </summary> public abstract class BaseContent { #region Properties /// <summary> /// get or set identifier of record /// </summary> public virtual long Id { get; set; } /// <summary> /// gets or sets date of publishing content /// </summary> public virtual DateTime PublishedOn { get; set; } /// <summary> /// gets or sets Last Update's Date /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets the blog pot body /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets the content title /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets value indicating Custom Slug /// </summary> public virtual string SlugUrl { get; set; } /// <summary> /// gets or sets meta title for seo /// </summary> public virtual string MetaTitle { get; set; } /// <summary> /// gets or sets meta keywords for seo /// </summary> public virtual string MetaKeywords { get; set; } /// <summary> /// gets or sets meta description of the content /// </summary> public virtual string MetaDescription { get; set; } /// <summary> /// gets or sets /// </summary> public virtual string FocusKeyword { get; set; } /// <summary> /// gets or sets value indicating whether the content use CanonicalUrl /// </summary> public virtual bool UseCanonicalUrl { get; set; } /// <summary> /// gets or sets CanonicalUrl That the Post Point to it /// </summary> public virtual string CanonicalUrl { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Follow for Seo /// </summary> public virtual bool UseNoFollow { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Index for Seo /// </summary> public virtual bool UseNoIndex { get; set; } /// <summary> /// gets or sets value indicating whether the content in sitemap /// </summary> public virtual bool IsInSitemap { get; set; } /// <summary> /// gets or sets a value indicating whether the content comments are allowed /// </summary> public virtual bool AllowComments { get; set; } /// <summary> /// gets or sets a value indicating whether the content comments are allowed for anonymouses /// </summary> public virtual bool AllowCommentForAnonymous { get; set; } /// <summary> /// gets or sets viewed count by rss /// </summary> public virtual long ViewCountByRss { get; set; } /// <summary> /// gets or sets viewed count /// </summary> public virtual long ViewCount { get; set; } /// <summary> /// Gets or sets the total number of comments /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.Approved).Count() /// We use this property for performance optimization (no SQL command executed) /// </remarks> /// </summary> public virtual int ApprovedCommentsCount { get; set; } /// <summary> /// Gets or sets the total number of comments /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.UnApproved).Count() /// We use this property for performance optimization (no SQL command executed)</remarks></summary> public virtual int UnApprovedCommentsCount { get; set; } /// <summary> /// gets or sets value indicating whether the content is logical deleted or hidden /// </summary> public virtual bool IsDeleted { get; set; } /// <summary> /// gets or sets rating complex instance /// </summary> public virtual Rating Rating { get; set; } /// <summary> /// gets or sets value indicating whether the content show with rssFeed /// </summary> public virtual bool ShowWithRss { get; set; } /// <summary> /// gets or sets value indicating maximum days count that users can send comment /// </summary> public virtual int DaysCountForSupportComment { get; set; } /// <summary> /// gets or sets information of User-Agent /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets icon name with size 200*200 px for snippet /// </summary> public virtual string SocialSnippetIconName { get; set; } /// <summary> /// gets or sets title for snippet /// </summary> public virtual string SocialSnippetTitle { get; set; } /// <summary> /// gets or sets description for snippet /// </summary> public virtual string SocialSnippetDescription { get; set; } /// <summary> /// gets or sets body of content's comment /// </summary> public virtual byte[] RowVersion { get; set; } /// <summary> /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance /// </summary> public virtual string TagNames { get; set; } /// <summary> /// gets or sets counter for Content's report /// </summary> public virtual int ReportsCount { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set user that create this record /// </summary> public virtual User Author { get; set; } /// <summary> /// gets or sets Id of user that create this record /// </summary> public virtual long AuthorId { get; set; } /// <summary> /// get or set the tags integrated with content /// </summary> public virtual ICollection<Tag> Tags { get; set; } #endregion }
بخشهای مختلفی که در ابتدای مقاله مطرح شدند، دارای یکسری خصوصیات مشترک میباشند و برای این منظور این خصوصیات را در یک کلاس پایه کپسوله کردهایم. شاید تفکر شما این باشد که میخواهیم ارث بری TPH یا TPT را اعمال کنیم. ولی با توجه به سلیقهی شخصی، در این بخش قصد استفاده از ارث بری را ندارم.
نکتهای که وجود دارد فیلدهای ApprovedCommentsCount UnApprovedCommentsCount و TagNames میباشند که هنگام درج نظر جدید باید تعداد نظرات ذخیره شده را ویرایش کنیم و هنگام ویرایش خود پست یا خبر با ... و یا حتی ویرایش خود تگ یا حذف آن تگ باید TagNames که لیست برچسبهای محتوا را به صورت جدا شده با (,) از هم دیگر میباشد، ویرایش کنیم (جای بحث دارد).
مشخص است که هر یک از مطالب منتشر شده در بخشهای وبلاگ، اخبار، نظرسنجی و آگهیها، یک کابر ایجاد کننده (Author نامیدهایم) خواهد داشت و هر کاربر هم میتواند چندین مطلب را ایجاد کند. لذا رابطهی یک به چند بین تمام این بخشها مذکور و کاربر ایجاد خواهد شد.
مدل LinkBack
/// <summary> /// Represents link for implemention linkback /// </summary> public class LinkBack { #region Ctor /// <summary> /// create one instance of <see cref="LinkBack"/> /// </summary> public LinkBack() { CreatedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets link's Id /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets text for show Link /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets link's address /// </summary> public virtual string Url { get; set; } /// <summary> /// gets or set value indicating whether this link is internal o external /// </summary> public virtual LinkBackType Type { get; set; } /// <summary> /// gets or sets date that this record is added /// </summary> public virtual DateTime CreatedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Post that associated /// </summary> public virtual BlogPost Post { get; set; } /// <summary> /// gets or sets id of Post that associated /// </summary> public virtual long PostId { get; set; } #endregion } /// <summary> /// represents Type of ReferrerLinks /// </summary> public enum LinkBackType { /// <summary> /// Internal link /// </summary> Internal, /// <summary> /// External Link /// </summary> External }
مطمئنا در خیلی از وبلاگها مثل سایت جاری متوجه نمایش لینکها ارجاع دهندههای خارجی و داخلی در زیر مطلب شدهاید. کلاس LinkBack هم دقیقا برای این منظور در نظرگرفته شده است که عنوان صفحهای که این پست در آنجا لینک داده شده است، به همراه آدرس آن صفحه، در جدول حاصل از این کلاس ذخیره خواهند شد. نوع داده LinkBackType هم برای متمایز کردن رکوردهای درج شده به عنوان LinkBack در نظر گرفته شده است که بتوان آنها را متمایز کرد، به ارجاعات داخلی و خارجی.
مدل پست ها
/// <summary> /// Represents a blog post /// </summary> public class BlogPost : BaseContent { #region Ctor /// <summary> /// Create one Instance of <see cref="BlogPost"/> /// </summary> public BlogPost() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets Status of LinkBack Notifications /// </summary> public virtual LinkBackStatus LinkBackStatus { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set blog post's Reviews /// </summary> public virtual ICollection<BlogComment> Comments { get; set; } /// <summary> /// get or set collection of links that reference to this blog post /// </summary> public virtual ICollection<LinkBack> LinkBacks { get; set; } /// <summary> /// get or set Collection of Users that Contribute on this post /// </summary> public virtual ICollection<User> Contributors { get; set; } #endregion } /// <summary> /// Represents Status for ReferrerLinks /// </summary> public enum LinkBackStatus { [Display(Name ="غیرفعال")] Disable, [Display(Name = "فعال")] Enable, [Display(Name = "لینکها داخلی")] JustInternal, [Display(Name = "لینکها خارجی")] JustExternal }
/// <summary> /// Represents a base class for every comment in system /// </summary> public abstract class BaseComment { #region Properties /// <summary> /// get or set identifier of record /// </summary> public virtual long Id { get; set; } /// <summary> /// gets or sets date of creation /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets displayName of this comment's Creator if he/she is Anonymous /// </summary> public virtual string CreatorDisplayName { get; set; } /// <summary> /// gets or sets body of blog post's comment /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets body of blog post's comment /// </summary> public virtual Rating Rating { get; set; } /// <summary> /// gets or sets informations of agent /// </summary> public virtual string UserAgent { get; set; } /// <summary> /// gets or sets siteUrl of Creator if he/she is Anonymous /// </summary> public virtual string SiteUrl { get; set; } /// <summary> /// gets or sets Email of Creator if he/she is anonymous /// </summary> public virtual string Email { get; set; } /// <summary> /// gets or sets status of comment /// </summary> public virtual CommentStatus Status { get; set; } /// <summary> /// gets or sets Ip Address of Creator /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or sets datetime that is modified /// </summary> public virtual DateTime? ModifiedOn { get; set; } /// <summary> /// gets or sets counter for report this comment /// </summary> public virtual int ReportsCount { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set user that create this record /// </summary> public virtual User Creator { get; set; } /// <summary> /// get or set Id of user that create this record /// </summary> public virtual long? CreatorId { get; set; } #endregion } public enum CommentStatus { /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */ [Display(Name = "تأیید شده")] Approved = 0, [Display(Name = "در انتظار بررسی")] Pending = 1, [Display(Name = "جفنگ")] Spam = 2, [Display(Name = "زباله دان")] Trash = -1 }
/// <summary> /// Represents a blog post's comment /// </summary> public class BlogComment : BaseComment { #region Ctor /// <summary> /// Create One Instance for <see cref="BlogComment"/> /// </summary> public BlogComment() { Rating = new Rating(); CreatedOn = DateTime.Now; } #endregion #region NavigationProperties /// <summary> /// gets or sets BlogComment's identifier for Replying and impelemention self referencing /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets blog's comment for Replying and impelemention self referencing /// </summary> public virtual BlogComment Reply { get; set; } /// <summary> /// get or set collection of blog's comment for Replying and impelemention self referencing /// </summary> public virtual ICollection<BlogComment> Children { get; set; } /// <summary> /// gets or sets post that this comment sent to it /// </summary> public virtual BlogPost Post { get; set; } /// <summary> /// gets or sets post'Id that this comment sent to it /// </summary> public virtual long PostId { get; set; } #endregion }
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.
مدتی هست که در حال تهیه یک کتابخانه گزارشگیری بر پایه iTextSharp هستم. برای تهیه backlog هم چه جایی بهتر از بررسی سؤالات موجود در انجمنها؛ چیزی مثل این:
بله، تاپیکی با 13 صفحه که حتی یک مورد از درخواستهای آن هم دارای پاسخ نبود؛ اما باز هم کاربران با علاقه هرچه تمامتر یا میدونید، از روی عجز درخواستشون رو مطرح میکردند و کسی نبود که جواب بده. حقیقتش این است که مشکل از افراد نیست یا اینکه «کسی نبود» یا «کسی نخواست» که جواب بده. مشکل این است که اکثر برنامههای گزارشگیری یا گزارش سازی موجود در حد یک Demo ware هستند. «نمیتونند» با مشکلات واقعی کاری موجود (در طی 13 صفحه که ذکر شد) راحت کنار بیان و راه حل بدرد بخوری رو ارائه بدن.