public class UseAmpImageAttribute : ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); // Change images to Amp-specific images response = UpdateAmpImages(response); // For AMP pages, change Script content to a link. response = ReplaceWithLink("script", response); // For AMP pages, change iFrame content to a link. response = ReplaceWithLink("iframe", response); _output.Write(response); } private string ReplaceWithLink(string tag, string response) { var doc = GetHtmlDocument(response); var elements = doc.DocumentNode.Descendants(tag); foreach (var htmlNode in elements) { if (htmlNode.Attributes["data-link"] == null) continue; var dataLink = htmlNode.Attributes["data-link"].Value; var paragraph = doc.CreateElement("p"); var text = String.Format("[Embedded Link] {0}", dataLink); var anchor = doc.CreateElement("a"); anchor.InnerHtml = text; anchor.Attributes.Add("href", dataLink); anchor.Attributes.Add("title", text); paragraph.InnerHtml = anchor.OuterHtml; var original = htmlNode.OuterHtml; var replacement = paragraph.OuterHtml; response = response.Replace(original, replacement); } return response; } private string UpdateAmpImages(string response) { // Use HtmlAgilityPack (install-package HtmlAgilityPack) var doc = GetHtmlDocument(response); var imageList = doc.DocumentNode.Descendants("img"); const string ampImage = "amp-img"; if (!imageList.Any()) return response; if (!HtmlNode.ElementsFlags.ContainsKey("amp-img")) { HtmlNode.ElementsFlags.Add("amp-img", HtmlElementFlag.Closed); } foreach (var imgTag in imageList) { var original = imgTag.OuterHtml; var replacement = imgTag.Clone(); replacement.Name = ampImage; replacement.Attributes.Remove("caption"); response = response.Replace(original, replacement.OuterHtml); } return response; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
یک نکتهی تکمیلی: روش لغو صف درخواستهای مکرر fetch ارسالی به سمت سرور
ورودی جستجوی بالای صفحه را درنظر بگیرید که بهازای هربار ورود حرفی، یک درخواست fetch جدید را به سمت سرور ارسال میکند تا نتایج جستجوی حاصل را دریافت کند. مشکل اینجاست که ما تنها به آخرین درخواست fetch ارسال شدهی به سمت سرور نیاز داریم و نه به تمام درخواستهای دیگری که صادر شدهاند. به همین جهت این صف درخواستهای fetch قبلی، غیربهینه بوده و ترافیک بالایی را سبب میشوند. یک روش مواجه شدن با این مساله، استفاده از مفهومی به نام debounce است که در پشت صحنه، از یک تایمر استفاده میکند و فقط هر چند ثانیه یکبار، یک درخواست جدید را به همراه آخرین متن ورودی، به سمت سرور ارسال خواهد کرد. راه دیگری هم برای مواجه شدن با این مشکل، در مرورگرهای جدید پیشبینی شدهاست که AbortController نام دارد. با استفاده از آن میتوان «سیگنالی» را به صف درخواستهای پرتعداد fetch قبلی حاصل از ورود اطلاعات کاربر ارسال کنیم که ... «لغو شوید» و به سمت سرور ارسال نشوید.
برای توضیح بهتر آن، به مثال زیر دقت کنید:
<!DOCTYPE html> <html> <body> <input id="search" type="number" /> <script> const results = []; const search = document.getElementById("search"); let controller = new AbortController(); let signal = controller.signal; const onChange = () => { const value = search.value; if (value) { controller.abort(); controller = new AbortController(); signal = controller.signal; getPost(value, signal); } }; search.onkeyup = onChange; </script> </body> </html>
- در اینجا یک input box را داریم که ابتدا، یافت شده و سپس به رخداد onkeyup آن، متد onChange نسبت داده شدهاست تا هربار که کاربر، اطلاعاتی را وارد میکند، فراخوانی شود.
- در ابتدای اسکریپت هم نحوهی نمونه سازی شیء استاندارد جاوااسکریپتی AbortController و دسترسی به شیء signal آنرا مشاهده میکنید.
- در متد onChange، ابتدا مقدار جدید ورودی کاربر، دریافت میشود، سپس این AbortController، لغو میشود و بعد یک نمونهی جدید از آن ایجاد شده و مجددا به شیء signal آن دسترسی پیدا میکنیم تا آنرا به متد getPost ارسال کنیم. این متد هم چنین پیاده سازی را دارد:
const getPost = (value, signal) => { fetch( `https://site.com/search/${value}`, { signal } ); };
همانطور که مشاهده میکنید، تابع fetch، قابلیت پذیرش شیء signal را هم دارد. زمانیکه با هربار تایپ کاربر، متد ()controller.abort فراخوانی میشود، سیگنالی را به fetch «قبلی» متصل به آن ارسال میکند که ... دیگر به سمت سرور ارسال نشو و متوقف شو. با اینکار فقط آخرین ورودی کاربر، سبب بروز یک fetch موفق میشود و ترافیک ارسالی به سمت سرور کاهش پیدا میکند (چون تمام fetchهای قبلی، سیگنال abort را دریافت کردهاند)؛ مانند مثال زیر که کاربر، 5 بار حروفی را وارد کرده و به ازای هربار ورود حرفی، یک درخواست fetch جدید، ایجاد شده، اما ... فقط آخرین درخواست ارسالی او موفق بوده و نتیجهای را بازگشت داده و مابقی درخواستها ... abort شدهاند. این عملیات abort، در سمت کاربر اعمال میشود؛ یعنی اصلا درخواستی به سمت سرور ارسال نمیشود و این لغو درخواست، توسط برنامهی سمت سرور انجام نشدهاست.
<script type="text/javascript"> $(function() { $('#jqgStarWarsCharacters').jqGrid({ url: '@Url.Action("Characters", "StarWars")', datatype: 'json', mtype: 'POST', cmTemplate: { align: 'center' }, colNames: [ 'Name', 'Gender', 'Height', 'Weight', 'Birth Year', 'Skin Color', 'Hair Color', 'Eye Color' ], colModel: [ { name: 'Name' }, { name: 'Gender', sortable: false, formatter: demo.jqGrid.character.genderFormatter }, { name: 'Height', summaryType: 'avg' }, { name: 'Weight', summaryType: 'avg' }, { name: 'BirthYear' }, { name: 'SkinColor', sortable: false, formatter: demo.jqGrid.character.skinColorFormatter }, { name: 'HairColor', sortable: false, formatter: demo.jqGrid.character.hairColorFormatter }, { name: 'EyeColor', sortable: false, formatter: demo.jqGrid.character.eyeColorFormatter } ], caption: 'StarWars Characters', grouping: true, groupingView: { groupField: ['Gender'], groupDataSorted: true, groupColumnShow: [false], groupSummary: [true] }, footerrow: true, userDataOnFooter: true, jsonReader: { repeatitems: false, id: 'Id' }, pager: $('#jqgpStarWarsCharacters'), rowNum: 100, sortname: 'Id', sortorder: 'asc', viewrecords: true, height: '100%' }); }); </script>
توی startup پروژه هم با ارتقا به Core 3.0 متدها به صورت زیر نوشته شده که متد UseMVC قبلی رو حذف کردم
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddRazorPages(); services.AddControllers() .AddNewtonsoftJson(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseStaticFiles();
JqGridRequest.ParametersNames = new JqGridParametersNames() { PagesCount = "npage" }; app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=JavaScript}/{action=Basics}"); endpoints.MapControllerRoute( name: "default2", pattern: "{controller=StarWars}/{action=Characters}/{id?}"); endpoints.MapRazorPages(); }); }
کار با Kendo UI DataSource
[DisplayName("تاریخ درج")] public DateTime CreateDate { get; set; } [DisplayName("تاریخ انتشار")] public DateTime PublishDate { get; set; }
[DisplayName("نوع مطلب")] public string BlogType { get; set; }
<script type="text/javascript"> $(function () { var blogDataSource = new kendo.data.DataSource({ transport: { read: { url: "@Url.Action("GetLastBlogs", "Admin")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, parameterMap: function (options) { return kendo.stringify(options); } }, schema: { data: "data", total: "total", model: { fields: { "id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است "title": { type: "string" }, "content": { type: "string" }, "imagepath": { type: "string" }, "attachmentid": { type: "number" }, "createdate": { type: "string" }, "publishdate": { type: "date" }, "blogtype": { type: "string" }, "lastmodified": { type: "date" }, "visitcount": { type: "number" }, "isarchived": { type: "boolean" }, "ispublished": { type: "boolean" }, "isdeleted": { type: "boolean" }, "tags": { type: "string" } } } }, error: function (e) { alert(e.errorThrown); }, pageSize: 10, sort: { field: "id", dir: "desc" }, serverPaging: true, serverFiltering: true, serverSorting: true }); $("#report-grid").kendoGrid({ dataSource: blogDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, filterable: true, reorderable: true, columnMenu: true, columns: [ { field: "id", title: "شماره", width: "130px" }, { field: "title", title: "عنوان مطلب" }, { field: "createdate", title: "تاریخ درج" }, { field: "publishdate", title: "تاریخ انتشار" }, { field: "content", title: "خلاصه مطلب" }, { field: "blogtype", title: "نوع" }, { field: "lastmodified", title: "آخرین ویرایش" }, { field: "visitcount", title: "تعداد بازدید" }, { field: "isarchived", title: "آرشیو شده", template: '<input type="checkbox" #= isarchived ? checked="checked" : "" # disabled="disabled" ></input>' }, { field: "ispublished", title: "منتشر شده", template: '<input type="checkbox" #= ispublished ? checked="checked" : "" # disabled="disabled" ></input>' } ] }); }); </script>
bower یک فناوری منسوخ شدهاست و اگر بخواهیم bower.json ذکر شدهی در این مطلب را با package.json مربوط به npm جایگزین کنیم، به یک چنین محتوایی خواهیم رسید:
{ "name": "testwebapp", "version": "1.0.0", "description": "", "scripts": {}, "author": "", "license": "ISC", "dependencies": { "bootstrap": "^3.3.7", "jquery": "^2.2.4", "jquery-ajax-unobtrusive": "^3.2.4", "jquery-validation": "^1.17.0", "jquery-validation-unobtrusive": "^3.2.8" } }
سپس بر اساس مسیرهای پوشهی node_modules جدید، فایل bundleconfig.json چنین محتوایی را پیدا میکند:
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "wwwroot/css/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/jquery-validation/dist/jquery.validate.min.js", "node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js", "node_modules/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js", "node_modules/bootstrap/dist/js/bootstrap.min.js", "wwwroot/js/site.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - DNTCommon.Web.Core.TestWebApp</title> <link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" /> </head> <body> <div> @RenderBody() </div> <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
<Project Sdk="Microsoft.NET.Sdk.Web"> <Target Name="PrecompileScript" BeforeTargets="BeforeBuild"> <Exec Command="dotnet bundle" /> </Target> <ItemGroup> <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" /> </ItemGroup> </Project>
- چه کسانی این فریم ورک را توسعه میدهند؟ گوگل. این مورد خیلی مهم است. در این بین شاید Aurelia مدرنتر به نظر برسد اما تیم آن سابقهی خوبی در نگهداری محصولات قبلی آن ندارد. برای مثال Durandal آنها به طور کامل رها شده و الان Aurelia را شروع کردهاند.
- تیم Angular اینقدر شجاعت داشته که اقرار کند نگارش 1 آن مشکلات زیادی دارد و دست به یک بازنویسی کامل زدهاند. باید دقت کرد که چقدر اینکار برای یک تیم محبوب در وب مشکل است و قطعا نگارش 2 آن که با این حجم بالای اعتراضات عدم سازگاری با نگارش 1 آن تولید شدهاست، بسیاری از مشکلات نگارش قبلی را ندارد. همچنین همین مساله سازگاری آنرا با نگارشهای بعد از 2 نیز تضمین خواهد کرد. چون اینبار دست به یک طراحی مجدد زدهاند و ... این طراحی مجدد خیلی برای آنها گران تمام شدهاست (خصوصا از لحاظ حجم انتقادهای رسیده). بنابراین در آینده در زمینه سازگاری با نگارشهای قبلی به شدت محتاط خواهند بود. این موردی است که استفاده کنندگان از ReactJS به زودی با آن مواجه خواهند شد.
- گروه کاربری مصرف کنندگان آن. چه تعداد مقاله، ویدیوی آموزشی، انجمن رفع اشکال و امثال اینها را در مورد یک محصول میتوانید پیدا کنید؟ قطعا AngularJS در این زمینه حرف اول را میزند.
- Angular 2 بر مبنای استاندارد web component طراحی شدهاست که در دراز مدت نیز سبب برد AngularJS 2 خواهد شد و نیاز کمتر به بازنویسیهای کلی.
- Angular 2 برای کار با محصولات موبایل بهینه سازی شدهاست و مانند بوت استرپ 3 یک فریم ورک mobile first است.
- استفادهی از Type Script به عنوان زبان اول این پلتفرم (البته استفادهی از آن اجباری نیست). این مساله در دراز مدت سبب تولید کدهایی با کیفیت بالاتر میشود. برای مثال الان ReactJS مخلوطی است از ES 5 و ES 6. اما AngularJS 2 تصمیم بهتری را اتخاذ کردهاست. همین مساله سبب شدهاست که توسعهی Type Script توسط مایکروسافت سرعت بیشتری پیدا کند.
- داشتن امکانات توکار بیشتری نسبت به رقبا. برای مثال ReactJS یک کتابخانه است؛ اما AngularJS 2 یک فریم ورک کامل که تمام کتابخانههای جانبی رقبا را یکجا از روز اول دارد.
بنابراین آیا پس از ارائهی نهایی Angular 2، ارزش یادگیری را دارد؟ بله؛ حتما. ابتدا (و هم اکنون) اعتراضهای شدیدی در مورد عدم سازگاری آن با نگارشهای قبلی وجود خواهد داشت و پس از مدتی همه آنرا فراموش کرده و خودشان را وفق میدهند.
کار با Kendo UI DataSource
<script type="text/javascript"> $(function () { // var r = "12"; var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "@Url.Action("GetProducts", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', data: { param1: "dfvdf", param2: "val2" } // ارسال اطلاعات اضافی و سفارشی به سرور در حین درخواست }, create: { url: "@Url.Action("PostProduct","Home")", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url:// function (product) { "@Url.Action("UpdateProduct","Home")",//, +product.Id; //}, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (p) { return "@Url.Action("DeleteProduct","Home")/" + p.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (options) { return kendo.stringify(options); } }, schema: { parse: function (data) { return data; }, data: "Data", total: "Total", 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 }, editable: true }, "Discription": { type: "string", }, "Title": { type: "string", editable: false }, "GroupName": { type: "string", }, "Link": { type: "string" } } }, batch: false, }, error: function (e) { alert(e.errorThrown.stack); }, pageSize: 5, sort: { field: "Id", dir: "desc" } }); $("#report-grid").kendoGrid({ dataSource: productsDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, columns: [ { field: "Id", title: "#" }, { field: "Name", title: "Product" } ] }); }); </script>
ما مطابق آموزشی که در این مقاله داده شده از یک اکشن متد برای ذخیره عکس ارسالی تو یک پوشه و سپس برگشت دادن مسیر عکس و از یک اکشن متد دیگه برای ذخیره اطلاعاتی که قراره همراه با فرم ارسال بشن (به همراه مسیر عکس برگشت داده شده)، استفاده میکنیم
مشکلی که ما موقع استفاده از این افزونه باهاش برخوردیم اینه که گاهی اوقات و همونطور که انتظار میره اکشن متد (AddAvatars) که وظیفه ذخیره عکس رو داره اول اجرا میشه و اکشن متد (Add) که وظیفه ذخیره اطلاعات رو داره دوم، ولی گاهی اوقات این ترتیب به هم میریزه و ابتدا اطلاعات ارسالی ذخیره میشه و بعد اکشن متد ذخیره عکس اجرا میشه.
سناریوی ما هم تا حدی شبیه به سناریویی هست که آقای احمدی مطرح کردند، ولی همونطور که گفتیم مشکل اصلی اینه که اکشن متدها هر بار با ترتیبهای متفاوت فراخوانی میشن
<div class="container-fluid"> @using (Ajax.BeginForm("Add", "Authors", new AjaxOptions { UpdateTargetId = "result", InsertionMode = InsertionMode.Replace, HttpMethod = "POST" }, new { @class = "form-horizontal", id = "UploadFile" })) { @Html.AntiForgeryToken() <div class="control-group"> <label class="control-label" for="AuhtorFirstNameAndLastName">نام نویسنده</label> <div class="controls"> @Html.TextBoxFor(author => author.AuhtorFirstNameAndLastName, new { placeholder = "نام نویسنده" }) </div> @Html.ValidationMessageFor(author => author.AuhtorFirstNameAndLastName) </div> <div class="control-group"> <label class="control-label" for="Status">ارسال عکس</label> <div class="controls"> <input type="file" name="avatarFile" id="avatarFile" /> </div> <div> @*<input type="submit" name="btn-submit" value="ارسال" class="btn btn-success" />*@ <img id="loading" alt="1" src="Images/loading83.gif" style="display: none;" /> </div> </div> <div id="result"></div> <input type="submit" name="btn-submit" value="افزودن نویسنده" class="btn btn-success" /> <input type="button" name="btn-colose" id="btn-close" value="انصراف" class="btn btn-danger" onclick="$dialog.dialog('close');" /> } </div> <script type="text/javascript"> $('#UploadFile').submit(function () { $("#loading").show(); $.ajaxFileUpload({ url: "@Url.Action("AddAvatar","Authors")", secureuri: false, fileElementId: 'avatarFile', dataType: 'json', data: {}, success: function (data, status) { $("#loading").hide(); }, error: function (data, status, e) { $("#loading").hide(); } }); }); </script>
مدیریت کلیدهای کیبرد در جاوا اسکریپت
e.preventDefault() e.stopPropagation()
<html> <body> <div onclick="document.body.appendChild(outter)" style='border:solid 1px'> Outer Div <a id="inner" href="index2.htm">index2</a> </div> <script> var outter = document.createElement('div'); outter.innerText = 'outter'; var inner = document.createElement('div'); inner.innerText = 'inner'; document.getElementById('inner').onclick = function(e) { e = e || window.event; document.body.appendChild(inner); //e.preventDefault(); //e.stopPropagation(); //e.returnValue = false; return false; }; </script> </body> </html>
روش ایجاد یک پروژهی کتابخانهای، از کامپوننتهای Blazor
اگر از ویژوال استودیو استفاده میکنید، نوع «Razor Class Library»، پروژههای مخصوص کتابخانههای کامپوننتهای Blazor را آغاز میکند و اگر میخواهید از CLI استفاده کنید، باید از دستور «dotnet new razorclasslib» استفاده کرد؛ که قابلیت تبدیل به بستههای نیوگت را با دستور dotnet pack داشته و به این ترتیب میتوان آنها را به اشتراک گذاشت و یا حتی میتوان ارجاعی از این پروژه را در سایر پروژههای شخصی، مورد استفاده قرار داد.
ساختار پیشفرض فایل csproj اینگونه پروژهها به صورت زیر است:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <SupportedPlatform Include="browser" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.6" /> </ItemGroup> </Project>
روش افزودن فایلهای ثابت مورد استفادهی در کتابخانه
پروژهی پیشفرضی که در اینجا ایجاد میشود، به همراه یک پوشهی wwwroot نیز هست. این پوشه محلی است که باید فایلهای ثابت اشتراکی پروژه را مانند فایلهای CSS و JS مورد استفاده، قرار داد.
این نوع پروژهها از ویژگی Isolated CSS و یا Isolated JS ارائه شدهی در Blazor 5x نیز پشتیبانی میکنند.
روش استفادهی از فایلهای ثابت موجود در یک کتابخانه
این فایلهای ثابت به همراه بستهی نهایی پروژه، به صورت خودکار توزیع میشوند (و نیازی به ارائهی مجزای آنها نیست) و برای استفادهی از آنها در پروژههای دیگر، باید از روش مسیردهی زیر استفاده کرد:
/_content/PackageId/MyImage.png
- PackageId عموما همان نام پروژهی مورد استفادهاست (نام فایل csproj مانند MyBlazorComponentLibrary). هرچند میتوان آنرا به صورت مجزایی در فایل csproj نیز مقدار دهی کرد.
- در این مثال MyImage.png، نام منبعی است که قرار است از آن استفاده کنیم و پیشتر در پوشهی wwwroot کتابخانه، کپی شدهاست و یا حتی میتوان زیر پوشههایی را نیز در اینجا ایجاد و از آنها استفاده کرد؛ مانند:
/_content/MyBlazorComponentLibrary/scripts/HelloWorld.js
<script src="_content/MyBlazorComponentLibrary/exampleJsInterop.js"></script>
روش استفادهی از کتابخانهی نهایی تولید شده
برای استفادهی از کتابخانهی نهایی تولید شده یا میتوان ارجاعی را به فایل csproj آن، به پروژهی خود افزود:
<ItemGroup> <ProjectReference Include="..\MyBlazorComponentLibrary\MyBlazorComponentLibrary.csproj" /> </ItemGroup>
پس از آن، جهت سهولت استفادهی از این کامپوننتهای اشتراکی، بهتر است فضای نام آنها را به فایل Imports.razor_ پروژهی خود افزود؛ تا نیازی به تعریف آنها در تمام کامپوننتهای مورد استفاده نباشد.