صورت مساله
public class Product { public int Id { set; get; } public DateTime AddDate { set; get; } public string Name { set; get; } public decimal Price { set; get; } }
میخواهیم زمانیکه فرمهای پویای ویرایش یا افزودن رکوردها ظاهر شدند، در حین تکمیل نام، یک auto complete ظاهر شود:
در حین ورود تاریخ، یک date picker شمسی جهت سهولت ورود اطلاعات نمایش داده شود:
همچنین در قسمت ورود مبلغ و قیمت، به صورت خودکار حرف سه رقم جدا کننده هزارها، نمایش داده شوند تا کاربران در حین ورود مبالغ بالا دچار اشتباه نشوند.
پیشنیازها
- برای نمایش auto complete از همان امکانات توکار jQuery UI که به همراه jqGrid عرضه میشوند، استفاده خواهیم کرد.
- برای نمایش date picker شمسی از مطلب «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده میکند» کمک خواهیم گرفت.
- جهت اعمال خودکار حرف سه رقم جدا کننده هزارها از افزونهی Price Format جیکوئری استفاده میکنیم.
تعریف و الحاق این پیشنیازها، فایل layout برنامه را به شکل زیر تغییر خواهد داد:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" /> <link href="~/Content/PersianDatePicker.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.7.2.min.js"></script> <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script> <script src="~/Scripts/i18n/grid.locale-fa.js"></script> <script src="~/Scripts/jquery.jqGrid.min.js"></script> <script src="~/Scripts/PersianDatePicker.js"></script> <script src="~/Scripts/jquery.price_format.2.0.js"></script> @RenderSection("Scripts", required: false) </body> </html>
تغییرات مورد نیاز سمت کلاینت، جهت اعمال افزونههای جیکوئری و سفارشی سازی عناصر دریافت اطلاعات
الف) نمایش auto complete در حین ورود نام محصولات
colModel: [ { name: 'Name', index: 'Name', align: 'right', width: 100, editable: true, edittype: 'text', editoptions: { maxlength: 40, dataInit: function (elem) { // http://jqueryui.com/autocomplete/ $(elem).autocomplete({ source: '@Url.Action("GetProductNames","Home")', minLength: 2, select: function (event, ui) { $(elem).val(ui.item.value); $(elem).trigger('change'); } }); } }, editrules: { required: true } } ],
برای پردازش سمت سرور آن و مقدار دهی url آن، یک چنین اکشن متدی را میتوان تدارک دید:
public ActionResult GetProductNames(string term) { var list = ProductDataSource.LatestProducts .Where(x => x.Name.StartsWith(term)) .Select(x => x.Name) .Take(10) .ToArray(); return Json(list, JsonRequestBehavior.AllowGet); }
ب) نمایش date picker شمسی در حین ورود تاریخ
colModel: [ { name: 'AddDate', index: 'AddDate', align: 'center', width: 100, editable: true, edittype: 'text', editoptions: { maxlength: 10, // https://www.dntips.ir/post/1382 onclick: "PersianDatePicker.Show(this,'@today');" }, editrules: { required: true } } ],
@{ ViewBag.Title = "Index"; var today = DateTime.Now.ToPersianDate(); }
ج) اعمال حروف سه رقم جدا کننده هزارها در حین ورود قیمت
colModel: [ { name: 'Price', index: 'Price', align: 'center', width: 100, formatter: 'currency', formatoptions: { decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$' }, editable: true, edittype: 'text', editoptions: { dir: 'ltr', dataInit: function (elem) { // http://jquerypriceformat.com/ $(elem).priceFormat({ prefix: '', thousandsSeparator: ',', clearPrefix: true, centsSeparator: '', centsLimit: 0 }); } }, editrules: { required: true, minValue: 0 } } ],
یک نکته
همین تعاریف را دقیقا به فرمهای جستجو نیز میتوان اعمال کرد. در اینجا برای حالات ویرایش و افزودن رکوردها، editoptions مقدار دهی شدهاست؛ در مورد فرمهای جستجو باید searchoptions و برای مثال dataInit آنرا مقدار دهی کرد.
مشکل مهم!
با تنظیمات فوق، قسمت UI بدون مشکل کار میکند. اما اگر در سمت سرور، مقادیر دریافتی را بررسی کنیم، نه تاریخ و نه قیمت، قابل دریافت نیستند. زیرا تاریخ ارسالی به سرور شمسی است و مدل برنامه DateTime میلادی میباشد. همچنین به دلیل وجود حروف سه رقم جدا کننده هزارها، عبارت دریافتی قابل تبدیل به عدد نیستند و مقدار دریافتی صفر خواهد بود.
برای رفع این مشکلات، نیاز به تغییر model binder توکار ASP.NET MVC است. برای تاریخها از کلاس PersianDateModelBinder میتوان استفاده کرد. برای اعداد decimal از کلاس ذیل:
using System; using System.Globalization; using System.Threading; using System.Web.Mvc; namespace jqGrid05.CustomModelBinders { /// <summary> /// How to register it in the Application_Start method of Global.asax.cs /// ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder()); /// </summary> public class DecimalBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType == typeof(decimal?)) { return bindDecimal(bindingContext); } return base.BindModel(controllerContext, bindingContext); } private static object bindDecimal(ModelBindingContext bindingContext) { var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult == null) return null; bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); decimal value; var valueAsString = valueProviderResult.AttemptedValue == null ? null : valueProviderResult.AttemptedValue.Trim(); if (string.IsNullOrEmpty(valueAsString)) return null; if (!decimal.TryParse(valueAsString, NumberStyles.Any, Thread.CurrentThread.CurrentCulture, out value)) { const string error ="عدد وارد شده معتبر نیست"; var ex = new InvalidOperationException(error, new Exception(error, new FormatException(error))); bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex); return null; } return value; } } }
برای ثبت و معرفی این کلاسها باید به نحو ذیل در فایل global.asax.cs برنامه عمل کرد:
using System; using System.Web.Mvc; using System.Web.Routing; using jqGrid05.CustomModelBinders; namespace jqGrid05 { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder()); ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder()); } } }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid05.zip
import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; @Injectable() export class AppConfigService { private config: IAppConfig; constructor(private http: HttpClient) { } loadClientConfig(): Promise<any> { return this.http.get<IAppConfig>("assets/client-config.json") .toPromise() .then(config => { this.config = config; console.log("Config", this.config); }) .catch(err => { return Promise.reject(err); }); } get configuration(): IAppConfig { if (!this.config) { throw new Error("Attempted to access configuration property before configuration data was loaded."); } return this.config; } } export interface IAppConfig { apiEndpoint: string; loginPath: string; logoutPath: string; refreshTokenPath: string; accessTokenObjectKey: string; refreshTokenObjectKey: string; adminRoleName: string; }
و تغییر ماژول CoreModule به شکل زیر :
import { NgModule, Optional, SkipSelf, APP_INITIALIZER } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; import { HTTP_INTERCEPTORS } from "@angular/common/http"; // import RxJs needed operators only once import "./services/rxjs-operators"; import { HeaderComponent } from "./component/header/header.component"; import { AuthGuard } from "./services/auth.guard"; import { AuthInterceptor } from "./services/auth.interceptor"; import { AuthService } from "./services/auth.service"; import { AppConfigService } from "./services/app-config.service"; import { BrowserStorageService } from "./services/browser-storage.service"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], declarations: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService, AppConfigService, AuthService, AuthGuard, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: APP_INITIALIZER, useFactory: (config: AppConfigService) => () => config.loadClientConfig(), deps: [AppConfigService ], multi: true } ] }) export class CoreModule { constructor( @Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
Cannot instantiate cyclic dependency! ApplicationRef ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
به کمک آن میتوان عبارات پیچیده T-SQL را Parse و آنالیز کرد. البته باید در نظر داشت هرچند این بسته جهت SQL Server 2012 ارائه شده اما این اسمبلی با نگارشهای 2005 به بعد اس کیوال سرور کاملا سازگار است و اساسا نیازی هم به SQL Server ندارد. در ادامه مروری خواهیم داشت بر نحوه استفاده از آن.
یافتن کوئریهای * Select در بین انبوهی از اسکریپتها به کمک SQLDom
در مورد مضرات کوئریهای * select پیشتر مطلبی را در این سایت خواندهاید. در ادامه قصد داریم به کمک امکانات اسمبلی Microsoft.SqlServer.TransactSql.ScriptDom.dll، تعدادی عبارت T-SQL را آنالیز کرده و مشخص کنیم که آیا حاوی * select هستند یا خیر. کد کامل آنرا در ذیل مشاهده میکنید:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Microsoft.SqlServer.TransactSql.ScriptDom; namespace DbCop { // Microsoft® SQL Server® 2012 Transact-SQL ScriptDom // SQL Server 2012 managed parser, Supports SQL Server 2005+ // SQLDom.msi (redist x86/x64) // http://www.microsoft.com/en-us/download/details.aspx?id=29065 // X86: http://go.microsoft.com/fwlink/?LinkID=239634&clcid=0x409 // X64: http://go.microsoft.com/fwlink/?LinkID=239635&clcid=0x409 // Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll class Program { static void Main() { const string tSql = @" -- select * in PROCEDURE CREATE PROCEDURE dbo.SelectStarTest AS SELECT * FROM dbo.tbl1 go -- select * in PROCEDURE with TableVar Create PRocedure SelectAll AS Declare @X table(Id integer) Select * from @x go -- select * in PROCEDURE with ctex CREATE PROCEDURE dbo.SelectAllCte AS WITH ctex AS ( SELECT * FROM sys.objects ) SELECT * FROM ctex go -- normal select * select * from tbl1; select * from dbo.tbl2; "; IList<ParseError> errors; TSqlScript sqlFragment; using (var reader = new StringReader(tSql)) { var parser = new TSql110Parser(initialQuotedIdentifiers: true); sqlFragment = (TSqlScript)parser.Parse(reader, out errors); } if (errors != null && errors.Any()) { var sb = new StringBuilder(); foreach (var error in errors) sb.AppendLine(error.Message); throw new InvalidOperationException(sb.ToString()); } var i = 0; foreach (var batch in sqlFragment.Batches) { Console.WriteLine("Batch: {0}, Statement(s): {1}", ++i, batch.Statements.Count); foreach (var statement in batch.Statements) { processStatement(statement); } Console.WriteLine(); } Console.WriteLine("\nPress a key..."); Console.Read(); } private static void processStatement(TSqlStatement statement) { var createProcedureStatement = statement as CreateProcedureStatement; if (createProcedureStatement != null) { var statementList = createProcedureStatement.StatementList; foreach (var procedureStatement in statementList.Statements) { processStatement(procedureStatement); } } var selectStatement = statement as SelectStatement; if (selectStatement != null) { var query = selectStatement.QueryExpression; var selectElements = ((QuerySpecification)query).SelectElements; foreach (var selectElement in selectElements) { var expression = selectElement as SelectStarExpression; if (expression == null) continue; Console.WriteLine( "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}", expression.StartOffset, expression.StartLine, statementToString(selectStatement)); } } } private static string statementToString(TSqlFragment selectStatement) { var text = new StringBuilder(); for (var i = selectStatement.FirstTokenIndex; i <= selectStatement.LastTokenIndex; i++) { text.Append(selectStatement.ScriptTokenStream[i].Text); } return text.ToString(); } } }
توضیحات:
پس از نصب SQLDom.msi، ارجاعی را به اسمبلی زیر اضافه نمائید تا بتوانید کد فوق را کامپایل کنید:
Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll
کار با ایجاد وهلهای از TSql110Parser شروع میشود. متد Parse آن، آرگومانی از نوع TextReader را قبول میکند. برای مثال با استفاده از StringReader میتوان محتوای یک متغیر رشتهای را به آن ارسال کرد و یا توسط StreamReader یک فایل sql را.
پس از فراخوانی متد Parse، بهتر است بررسی شود که آیا عبارت T-SQL دریافتی معتبر بوده است یا خیر. اینکار را توسط لیستی از ParseErrorهای دریافتی میتوان انجام داد.
خروجی متد Parse، حاوی یک سری Batch آنالیز شده است. هر عبارت Go در اینجا یک Batch را تشکیل میدهد. سپس در داخل هر batch به دنبال batch.Statements خواهیم گشت تا بتوان به عبارات T-SQL آنها دسترسی یافت.
در ادامه کار اصلی توسط متد processStatement صورت میگیرد. عبارات دریافتی، در حالت کلی از نوع TSqlStatement هستند اما در اصل میتوانند یکی از مشتقات آن نیز باشند. در اینجا فقط دو مورد CreateProcedureStatement و SelectStatement بررسی شدهاند (مطابق رشته tSql ابتدای مثال). هر دو عبارت، از کلاس TSqlStatement مشتق شدهاند.
در متد processStatement عبارات select معمولی و همچنین آنهایی که داخل رویههای ذخیره شده تعریف شدهاند، استخراج شده و در نهایت بررسی میشوند که آیا از نوع SelectStarExpression هستند یا خیر (همان * select صورت مساله).
خروجی مثال فوق به شرح زیر است:
Batch: 1, Statement(s): 1 `Select *` detected @StartOffset:140, Line:5, T-SQL: SELECT * FROM dbo.tbl1 Batch: 2, Statement(s): 1 `Select *` detected @StartOffset:368, Line:12, T-SQL: Select * from @x Batch: 3, Statement(s): 1 `Select *` detected @StartOffset:659, Line:22, T-SQL: WITH ctex AS ( SELECT * FROM sys.objects ) SELECT * FROM ctex Batch: 4, Statement(s): 2 `Select *` detected @StartOffset:753, Line:26, T-SQL: select * from tbl1; `Select *` detected @StartOffset:791, Line:27, T-SQL: select * from dbo.tbl2;
- اصل Fork در اینجا
- تاریخچهی این Fork غیر رسمی در اینجا
- بستهی نیوگت آن در اینجا
چون تیم EF در نگارش فعلی این کتابخانه حاضر به افزودن این نوع جدید نشدهاست، بنابراین بجای بستهی اصلی Entity framework نیاز است بستهی EntityFrameworkWithHierarchyId را نصب کنید.
PM> install-package EntityFrameworkWithHierarchyId
یک تذکر مهم:
چون امضای دیجیتال این بسته، با امضای دیجیتال بستهی اصلی EF یکی نیست، اگر پروژهی شما صرفا از EF استفاده میکند، مشکلی نخواهید داشت. اما اگر برای مثال از ASP.NET Identity کامپایل شدهی برای کار با EF اصلی استفاده کنید، پیام یافت نشدن DLL مرتبط را دریافت خواهید کرد.
تعریفی مدلی با خاصیتی از نوع جدید HierarchyId
public class Employee { public int Id { get; set; } [Required, MaxLength(100)] public string Name { get; set; } [Required] public HierarchyId Node { get; set; } // نوع داده جدید }
تعریف Context و مقدار دهی اولیهی آن
در این حالت Context برنامه به همراه تنظیمات اولیهی Migrations آن یک چنین شکلی را پیدا خواهد کرد:
public class MyContext : DbContext { public DbSet<Employee> Employees { get; set; } public MyContext() : base("Connection1") { this.Database.Log = log => Console.WriteLine(log); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (context.Employees.Any()) return; context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD NodePath as Node.ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD Level AS Node.GetLevel() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNode as Node.GetAncestor(1) persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD ManagerNodePath as Node.GetAncestor(1).ToString() persisted"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] ADD CONSTRAINT [UK_EmployeeNode] UNIQUE NONCLUSTERED (Node)"); context.Database.ExecuteSqlCommand( "ALTER TABLE [dbo].[Employees] WITH CHECK ADD CONSTRAINT [EmployeeManagerNodeNodeFK] " + "FOREIGN KEY([ManagerNode]) REFERENCES [dbo].[Employees] ([Node])"); context.Employees.Add(new Employee { Name = "Root", Node = new HierarchyId("/") }); context.Employees.Add(new Employee { Name = "Emp1", Node = new HierarchyId("/1/") }); context.Employees.Add(new Employee { Name = "Emp2", Node = new HierarchyId("/2/") }); context.Employees.Add(new Employee { Name = "Emp3", Node = new HierarchyId("/1/1/") }); context.Employees.Add(new Employee { Name = "Emp4", Node = new HierarchyId("/1/1/1/") }); context.Employees.Add(new Employee { Name = "Emp5", Node = new HierarchyId("/2/1/") }); context.Employees.Add(new Employee { Name = "Emp6", Node = new HierarchyId("/1/2/") }); base.Seed(context); } }
همچنین چند فیلد محاسباتی نیز بر اساس امکانات توکار SQL Server اضافه شدهاند. متدهایی مانند ToString، GetLevel، GetAncestor و امثال آن جزئی از پیاده سازی توکار SQL Server هستند. همچنین این متدها توسط کتابخانهی EntityFrameworkWithHierarchyId نیز ارائه شدهاند.
کوئری نویسی
مرتب سازی رکوردها بر اساس HierarchyId آنها
using (var context = new MyContext()) { Console.WriteLine("\ngetItems OrderByDescending(employee => employee.Node)"); var employees = context.Employees.OrderByDescending(employee => employee.Node).ToList(); foreach (var employee in employees) { Console.WriteLine("{0} {1}", employee.Id, employee.Node); } }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] ORDER BY [Extent1].[Node] DESC 6 /2/1/ 3 /2/ 7 /1/2/ 5 /1/1/1/ 4 /1/1/ 2 /1/ 1 /
یافتن یک HierarchyId خاص و سپس یافتن کلیهی فرزندان آن در یک سطح پایینتر
using (var context = new MyContext()) { Console.WriteLine("\nGetAncestor(1) of /1/"); var firstItem = context.Employees.Single(employee => employee.Node == new HierarchyId("/1/")); foreach (var item in context.Employees.Where(employee => firstItem.Node == employee.Node.GetAncestor(1))) { Console.WriteLine("{0} {1}", item.Id, item.Name); } }
با این خروجی:
SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE cast('/1/' as hierarchyid) = [Extent1].[Node] SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (@p__linq__0 = ([Extent1].[Node].GetAncestor(1))) OR ((@p__linq__0 IS NULL) AND ([Extent1].[Node].GetAncestor(1) IS NULL)) -- p__linq__0: '/1/' (Type = Object) 4 Emp3 7 Emp6
کوئریهای فوق را میتوان بجای استفاده از متد GetAncestor، با استفاده از متد IsDescendantOf به شکل زیر نیز نوشت:
var list = context.Employees.Where( employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")) && employee.Node.GetLevel() == 2).ToList();
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Node] AS [Node] FROM [dbo].[Employees] AS [Extent1] WHERE (([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1) AND (2 = ([Extent1].[Node].GetLevel()))
جابجا کردن نودها توسط متد GetReparentedValue
در کوئری ذیل، تمامی فرزندان ریشهی /1/ یافت شده و سپس والد آنها به صورت پویا تغییر داده میشود:
var items = context.Employees.Where(employee => employee.Node.IsDescendantOf(new HierarchyId("/1/"))) .Select(employee => new { Id = employee.Id, OrigPath = employee.Node, ReparentedValue = employee.Node.GetReparentedValue(new HierarchyId("/1/"), HierarchyId.GetRoot()), Level = employee.Node.GetLevel() }).ToList(); foreach (var item in items) { Console.WriteLine("Id:{0}; OrigPath:{1}; ReparentedValue:{2}; Level:{3}", item.Id, item.OrigPath, item.ReparentedValue, item.Level); }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Node] AS [Node], [Extent1].[Node].GetReparentedValue(cast('/1/' as hierarchyid), hierarchyid::GetRoot()) AS [C1], [Extent1].[Node].GetLevel() AS [C2] FROM [dbo].[Employees] AS [Extent1] WHERE ([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1 Id:2; OrigPath:/1/; ReparentedValue:/; Level:1 Id:4; OrigPath:/1/1/; ReparentedValue:/1/; Level:2 Id:5; OrigPath:/1/1/1/; ReparentedValue:/1/1/; Level:3 Id:7; OrigPath:/1/2/; ReparentedValue:/2/; Level:2
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
HierarcyIdTests.zip
مدل برنامه و نیازمندیهای اعتبارسنجی آن
namespace jqGrid08.Models { public class User { public int Id { set; get; } public string Name { set; get; } public string Email { set; get; } public string Password { set; get; } public string SiteUrl { set; get; } } }
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شدهی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمهی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمهی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.
اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Name))', index: '@(StronglyTyped.PropertyName<User>(x => x.Name))', align: 'right', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 40 }, editrules: { required: true, custom: true, custom_func: function (value, colname) { if (!value) return [false, "لطفا نامی را وارد کنید"]; if (value.length < 3 || value.length > 40) return [false, colname + " باید بین 3 تا 40 حرف باشد"]; else return [true, ""]; } } }, ],
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان میرسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالتهای Add و Edit را بررسی کنیم:
$('#list').jqGrid({ // ... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { afterSubmit: showServerSideErrors }, //add options { afterSubmit: showServerSideErrors }, //delete options { }); }); function showServerSideErrors(response, postdata) { var result = $.parseJSON(response.responseText); if (result.success === false) { //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن //و همچنین جلوگیری از ثبت نهایی فرم return [false, result.message, result.id]; } return [true, "", result.id]; }
[HttpPost] public ActionResult AddUser(User postData) { //todo: Add user to repository if (postData == null) return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet); if (_usersInMemoryDataSource.Any( user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase))) { return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet); } if (_usersInMemoryDataSource.Any( user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase))) { return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet); } postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1; _usersInMemoryDataSource.Add(postData); return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); }
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.
اعتبار سنجی ایمیل کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Email))', index: '@(StronglyTyped.PropertyName<User>(x => x.Email))', align: 'center', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 250, dir: 'ltr' }, editrules: { required: true, email: true }, formatter: 'email' }, ],
مطابق نیازمندیهای اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت میشود.
اعتبار سنجی کلمه عبور کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.Password))', index: '@(StronglyTyped.PropertyName<User>(x => x.Password))', align: 'center', width: 70, editable: true, edittype: 'password', editoptions: { maxlength: 10, dir: 'ltr' }, editrules: { //required: true ---> در این حالت خاص قابل استفاده نیست //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است //در حالت افزودن رکورد، ورود کلمه عبور اجباری است } }, ],
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
$('#list').jqGrid({ // ... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { /*, beforeSubmit: function (posdata, obj) { //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است return [true, ""]; }*/ }, //add options { beforeSubmit: function (postdata, obj) { //در حالت افزودن رکورد، ورود کلمه عبور اجباری است if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined) return [false, "لطفا کلمه عبور را وارد کنید"]; return [true, ""]; } }, //delete options { }); });
اعتبار سنجی آدرس سایت کاربر
colModel: [ { name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))', index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))', align: 'center', width: 150, editable: true, edittype: 'text', editoptions: { maxlength: 1000, dir: 'ltr' }, editrules: { required: true, url: true }, formatter: function (cellvalue, options, rowObject) { return "<a href='" + cellvalue + "' >" + cellvalue + "</a>"; }, unformat: function (cellvalue, options, cell) { return $('a', cell).attr('href'); } }, ],
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid08.zip
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET
اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شدهاست. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.
قسمت عمده مطلب جاری با پیشنیازهای یاد شده فوق یکی است. در اینجا فقط به ذکر تفاوتها بسنده خواهد شد.
الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.
ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمتهای جستجو شده، یکی است:
using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class CreateIndex { static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30; public static Document MapPostToDocument(Post post) { var postDocument = new Document(); postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS); titleField.Boost = 3; postDocument.Add(titleField); postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); return postDocument; } public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path) { var directory = FSDirectory.Open(new DirectoryInfo(path)); var analyzer = new StandardAnalyzer(_version); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { foreach (var post in dataList) { writer.AddDocument(MapPostToDocument(post)); } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } } } }
ج) تهیه قسمت منبع داده Auto-Complete
namespace LuceneSearch.Core.Model { public class SearchResult { public int Id { set; get; } public string Title { set; get; } } }
using System.Collections.Generic; using System.IO; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using LuceneSearch.Core.Model; using LuceneSearch.Core.Utils; namespace LuceneSearch.Core { public static class AutoComplete { private static IndexSearcher _searcher; /// <summary> /// Get terms starting with the given prefix /// </summary> /// <param name="prefix"></param> /// <param name="maxItems"></param> /// <returns></returns> public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10) { if (_searcher == null) _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true); var resultsList = new List<SearchResult>(); if (string.IsNullOrWhiteSpace(prefix)) return resultsList; prefix = prefix.ApplyCorrectYeKe(); var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems); if (results.TotalHits == 0) { results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems); } foreach (var doc in results.ScoreDocs) { resultsList.Add(new SearchResult { Title = _searcher.Doc(doc.Doc).Get("Title"), Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id")) }); } return resultsList; } } }
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آنرا در کدهای فوق ملاحظه میکنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.
د) نمایش Auto-Complete در ASP.NET MVC
using System.Text; using System.Web.Mvc; using LuceneSearch.Core; using System.Web; namespace LuceneSearch.Controllers { public class HomeController : Controller { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public ActionResult Index(int? id) { if (id.HasValue) { //todo: do something } return View(); //Show the page } public virtual ActionResult ScoredTerms(string q) { if (string.IsNullOrWhiteSpace(q)) return Content(string.Empty); var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http"); result.AppendLine(item.Title + "|" + postUrl); } return Content(result.ToString()); } } }
@{ ViewBag.Title = "جستجو"; var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home"); var bulletImage = Url.Content("~/Content/Images/bullet_shape.png"); } <h2> جستجو</h2> <div align="center"> @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" }) <br /> جهت آزمایش lu را وارد نمائید </div> @section scripts { <script type="text/javascript"> EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage'); </script> }
function EnableSearchAutocomplete(url, img) { var formatItem = function (row) { if (!row) return ""; return "<img src='" + img + "' /> " + row[0]; } $(document).ready(function () { $("#term").autocomplete(url, { dir: 'rtl', minChars: 2, delay: 5, mustMatch: false, max: 20, autoFill: false, matchContains: false, scroll: false, width: 300, formatItem: formatItem }).result(function (evt, row, formatted) { if (!row) return; window.location = row[1]; }); }); }
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده میکنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl); return Content(result.ToString());
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم میباشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم میباشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه میکنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفهای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کردهایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آنرا نیز ملاحظه میکنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره میکند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه میکنید (در متد result مقدار دهی شده). window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره میکند.
ه) نمایش Auto-Complete در ASP.NET WebForms
قسمت عمده مطالب فوق با وب فرمها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>جستجو</title> <link href="Content/Site.css" rel="stylesheet" type="text/css" /> <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script> <script src="Scripts/custom.js" type="text/javascript"></script> </head> <body dir="rtl"> <h2> جستجو</h2> <form id="form1" runat="server"> <div align="center"> <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox> <br /> جهت آزمایش lu را وارد نمائید </div> </form> <script type="text/javascript"> EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png'); </script> </body> </html>
using System.Text; using System.Web; using LuceneSearch.Core; namespace LuceneSearch.WebForms { public class Search : IHttpHandler { static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx"; public void ProcessRequest(HttpContext context) { string q = context.Request.QueryString["q"]; if (string.IsNullOrWhiteSpace(q)) { context.Response.Write(string.Empty); context.Response.End(); } var result = new StringBuilder(); var items = AutoComplete.GetTermsScored(_indexPath, q); foreach (var item in items) { var postUrl = "Default.aspx?id=" + item.Id; result.AppendLine(item.Title + "|" + postUrl); } context.Response.ContentType = "text/plain"; context.Response.Write(result.ToString()); context.Response.End(); } public bool IsReusable { get { return false; } } } }
در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl); context.Response.Write(result.ToString());
کدهای کامل مثال فوق را از اینجا میتوانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین میباشد که IIS Express 7.5 را نیز پیشتر نصب کردهاید.
کلمه عبور فایل: dotnettips91
سازماندهی بهتر کامپوننتهای ngx-bootstrap
پس از نصب بستهی npm کتابخانهی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» بررسی شدند، برای کار با کامپوننتهای این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام اینکار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن میتوان شبیه به مطلب «سازماندهی برنامههای Angular توسط ماژولها» در پوشهی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننتهای این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { BsDropdownModule } from "ngx-bootstrap/dropdown"; import { TooltipModule } from "ngx-bootstrap/tooltip"; import { ModalModule } from "ngx-bootstrap/modal"; @NgModule({ imports: [ CommonModule, BsDropdownModule.forRoot(), TooltipModule.forRoot(), ModalModule.forRoot() ], exports: [ BsDropdownModule, TooltipModule, ModalModule ] }) export class SharedBootstrapModule { }
اکنون برای استفادهی از SharedBootstrapModule اختصاصی فوق، میتوان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
import { SharedBootstrapModule } from './shared/shared.bootstrap.module'; @NgModule({ imports: [BrowserModule, SharedBootstrapModule], // ... }) export class AppModule {}
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامههای Angular توسط ماژولها» ایجاد کردهاید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
import { SharedBootstrapModule } from "./shared.bootstrap.module"; @NgModule({ imports: [ CommonModule, SharedBootstrapModule ], exports: [ CommonModule, SharedBootstrapModule ] })
نمایش یک modal dialog توسط کامپوننت Modal
پس از تعریف ModalModule.forRoot، اکنون میتوان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا میکند:
import { Component, OnInit, TemplateRef } from "@angular/core"; import { BsModalRef, BsModalService } from "ngx-bootstrap"; @Component({ selector: "app-modal-dialog-test", templateUrl: "./modal-dialog-test.component.html", styleUrls: ["./modal-dialog-test.component.css"] }) export class ModalDialogTestComponent implements OnInit { modalRef: BsModalRef; constructor(private modalService: BsModalService) { } openModal(template: TemplateRef<any>) { this.modalRef = this.modalService.show(template, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }); } closeModal() { this.modalRef.hide(); } }
سپس با فراخوانی متد this.modalService.show میتوان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت میدهد. از این ارجاع میتوان در جهت بستن آن استفاده کرد (مانند متد closeModal).
بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا میکند:
<h1>Displaying modal bootstrap dialogs</h1> <button type="button" class="btn btn-info" (click)="openModal(template1)">Create template modal</button> <ng-template #template1> <div class="modal-header"> <h4 class="modal-title pull-left">Modal</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> This is a modal. </div> </ng-template>
طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات
در ادامه میخواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آنرا در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشهی Shared ایجاد میکنیم:
ng g c Shared/ConfirmModal --skip-import
import { ConfirmModalComponent } from "./confirm-modal/confirm-modal.component"; @NgModule({ imports: [ ], entryComponents: [ ConfirmModalComponent ], declarations: [ ConfirmModalComponent ] }) export class SharedModule {}
این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف میشود:
import { Component } from "@angular/core"; @Component({ selector: "app-confirm-modal", templateUrl: "./confirm-modal.component.html", styleUrls: ["./confirm-modal.component.css"] }) export class ConfirmModalComponent { args: { title: string; message: string; }; close: (val?: any) => void; }
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف میشود:
<div class="modal-header"> <h4 class="modal-title pull-left">{{ args?.title }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="close()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <p>{{ args?.message }}</p> </div> <div class="modal-footer"> <button class="btn btn-danger" (click)="close(true)">Yes</button> <button class="btn btn-primary" (click)="close()">Cancel</button> </div>
تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننتهای مودال طراحی کنیم. این سرویس عمومی در پوشهی Core و CoreModule ثبت خواهد شد:
>ng g s Core/Modal
import { Injectable } from "@angular/core"; import { BsModalService } from "ngx-bootstrap"; @Injectable() export class ModalService { constructor(private bsModalService: BsModalService) { } show(component: any, args?: any, options?: any): Promise<any> { return new Promise(resolve => { options = options || {}; const modal = this.bsModalService.show(component, options); let result: any; const sub = this.bsModalService.onHidden.subscribe(() => { sub.unsubscribe(); resolve(result); }); modal.content.args = args; modal.content.close = (val?: any) => { result = val; modal.hide(); }; }); } }
یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمهی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شدهی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخداد this.bsModalService.onHidden فراخوانی میشود. در حالت الف، همان متد close درج شدهی در کامپوننت فراخوانی میشود.
برای اینکه بتوان نتیجهی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روشها، استفاده از Promiseها است که مشاهده میکنید. با فراخوانی resolve(result)، کار ارسال نتیجهی فراخوانی متدهای close(true) و ()close صورت میگیرد (یا true و یا undefined).
خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر میکند (content به کامپوننت بارگذاری شده اشاره میکند). اینجا است که میتوان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.
پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
import { ModalService } from "./modal.service"; @NgModule({ providers: [ ModalService ] }) export class CoreModule {}
<button type="button" class="btn btn-danger" (click)="deleteRecord()">Delete record</button> <div *ngIf="confirmResult" class="alert alert-info">{{confirmResult}}</div>
import { ModalService } from "./../../core/modal.service"; import { ConfirmModalComponent } from "./../../shared/confirm-modal/confirm-modal.component"; export class ModalDialogTestComponent implements OnInit { confirmResult: string; constructor(private modalService: ModalService) { } deleteRecord() { this.confirmResult = ""; this.modalService.show( ConfirmModalComponent, { title: "Confirm", message: "Do you want to delete this record?" }, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }).then(confirmed => { if (confirmed) { this.confirmResult = "Deleted!"; } else { this.confirmResult = "Canceled!"; } }); } }
توسط this.modalService.show میتوان انواع و اقسام کامپوننتهای مودال را به صورت پویا بارگذاری کرد و نمایش داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام میشود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار میگیرد؛ بجای استفاده از پروتکلهای استاندارد وب سرویسها مانند Soap و امثال آن. پیاده سازیهای ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شدهاست. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامههای ASP.NET.
یافتن آدرس وب سرویس سایت پذیرای Pingback
اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه میشود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
<link rel="pingback" href="pingback server">
همانطور که ملاحظه میکنید، نیاز است Response header را آنالیز کنیم.
private Uri findPingbackServiceUri() { var request = (HttpWebRequest)WebRequest.Create(_targetUri); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Get; request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; using (var response = request.GetResponse() as HttpWebResponse) { if (response == null) return null; var url = extractPingbackServiceUriFormHeaders(response); if (url != null) return url; if (!isResponseHtml(response)) return null; using (var reader = new StreamReader(response.GetResponseStream())) { return extractPingbackServiceUriFormPage(reader.ReadToEnd()); } } } private static Uri extractPingbackServiceUriFormHeaders(WebResponse response) { var pingUrl = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) || header.Equals("pingback", StringComparison.OrdinalIgnoreCase)); return getValidAbsoluteUri(pingUrl); } private static Uri extractPingbackServiceUriFormPage(string content) { if (string.IsNullOrWhiteSpace(content)) return null; var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase); var match = regex.Match(content); return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value); } private static Uri getValidAbsoluteUri(string url) { Uri absoluteUri; return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri; } private static bool isResponseHtml(WebResponse response) { var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("content-type", StringComparison.OrdinalIgnoreCase)); return !string.IsNullOrWhiteSpace(contentTypeKey) && response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase); }
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شدهاست. زمانیکه این صفحه را درخواست میکنیم، response.Headers.AllKeys حاصل میتواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان مییابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شدهاست.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شدهاست. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.
ارسال Ping به آدرس سرویس Pingback
اکنون که آدرس سرویس pingback یک سایت را یافتهایم، کافی است ping ایی را به آن ارسال کنیم:
public void Send() { var pingUrl = findPingbackServiceUri(); if (pingUrl == null) throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host)); sendPing(pingUrl); } private void sendPing(Uri pingUrl) { var request = (HttpWebRequest)WebRequest.Create(pingUrl); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml"; request.ProtocolVersion = HttpVersion.Version11; makeXmlRpcRequest(request); using (var response = (HttpWebResponse)request.GetResponse()) { response.Close(); } } private void makeXmlRpcRequest(WebRequest request) { var stream = request.GetRequestStream(); using (var writer = new XmlTextWriter(stream, Encoding.ASCII)) { writer.WriteStartDocument(true); writer.WriteStartElement("methodCall"); writer.WriteElementString("methodName", "pingback.ping"); writer.WriteStartElement("params"); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); } }
در اینجا sourceUri آدرس صفحهای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شدهاست. در یک pinback، صرفا این دو آدرس به سرویس دریافت کنندهی pingback ارسال میشوند.
سپس سایت دریافت کنندهی ping، ابتدا sourceUri را دریافت میکند تا عنوان آنرا استخراج کند و همچنین بررسی میکند که آیا targetUri، در آن درج شدهاست یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs
نحوهی استفاده از کلاس Pingback تهیه شده
کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش میشوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینکهای خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینکهای یک محتوا را نیز میتوان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانهی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url); var linkTags = doc.DocumentNode.Descendants("link"); var linkedPages = doc.DocumentNode.Descendants("a") .Select(a => a.GetAttributeValue("href", null)) .Where(u => !String.IsNullOrEmpty(u));
اگر بخواهید در کد وب پارت خود از ویژگی Session استفاده کنید ، ممکن است SharePoint این اجازه را به شما ندهد... راه حل چیست؟
اگر کد خود را بعد از پیاده سازی Session اجرا کنید چنین پیغامی را مشاهده خواهید کرد:
این پیغام نشان می دهد که تنظیمات Session در فایل web.config فعال نیست .یکی از راه حل های این مشکل چک کردن ویژگی enableSessionState="true" در هدر صفحه و در تگ Page است
اگر این ویژگی True باشد و همچنان همان پیغام فوق را مشاهده کردید به فایلweb.config وبسایت مورد نظر مراجعه کرده و به روش زیر عمل کنید :
// 1 - Find This Node <modules runAllManagedModulesForAllRequests="true"> // 2 - Add These Nodes <remove name="Session" /> <add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="" />
اکنون امکان استفاده از Session برای شما فراهم شده است .
در غیر این صورت باید تغییراتی در Permission Policy های شیرپوینت که در فایل های wss_minimaltrust.config و wss_mediumtrust.config و ... انجام دهید و یک Permission Set به آن اضافه کنید
این فایل ها در زیر مجموعه 14 و در پوشه CONFIG قرار دارند .
موفق باشید