خطای data binding
«نحوه نمایش فایلهای PDF»
تغییر نام یک فایل
در هنگام تغییر نام فایل در قسمتی که مسیر فایل نمایش داده شده هنگامی که بر روی نام فایل کلیک میکنیم یه خطای کنترل نشده نمایش داده میشود
لطفا بررسی فرماییدThe filename, directory name, or volume label syntax is incorrect.
تشکر
ایجاد پروژهی ASP.NET Core Web API
برای تامین اطلاعات برنامهی سمت کلاینت Blazor WASM و همچنین فراهم آوردن زیرساخت اعتبارسنجی کاربران آن، نیاز به یک پروژهی ASP.NET Core Web API داریم که آنرا با اجرای دستور dotnet new webapi در یک پوشهی خالی، برای مثال به نام BlazorWasm.WebApi ایجاد میکنیم.
البته این پروژه، از زیرساختی که در برنامهی Blazor Server بررسی شدهی تا این قسمت، ایجاد کردیم نیز استفاده خواهد کرد. همانطور که پیشتر نیز عنوان شد، هدف از قسمت Blazor Server مثال این سری، آشنایی با مدل برنامه نویسی خاص آن بود؛ وگرنه میتوان کل این پروژه را با Blazor Server و یا کل آنرا با Web API + Blazor WASM نیز پیاده سازی کرد. در این مثال، قسمتهای مدیریتی برنامهی مدیریت هتل را توسط Blazor Server (مانند قسمتهای تعریف اتاقها و امکانات رفاهی هتل) و قسمت مخصوص کاربران آنرا مانند رزرو کردن اتاقها، توسط Blazor WASM پیاده سازی میکنیم. به همین جهت قسمتهایی از این دو پروژه، مانند سرویسهای استفاده شدهی در پروژهی Blazor server، در پروژهی Web API مکمل Blazor WASM، قابلیت استفادهی مجدد را دارند.
افزودن سرویسهای آغازین مورد نیاز، به پروژهی Web API
در فایل آغازین BlazorWasm\BlazorWasm.WebApi\Startup.cs، برای شروع به تکمیل Web API، نیاز به این سرویسها را داریم:
namespace BlazorWasm.WebApi { public class Startup { //... public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(typeof(MappingProfile).Assembly); services.AddScoped<IHotelRoomService, HotelRoomService>(); services.AddScoped<IAmenityService, AmenityService>(); services.AddScoped<IHotelRoomImageService, HotelRoomImageService>(); var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //...
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\BlazorServer\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" /> <ProjectReference Include="..\..\BlazorServer\BlazorServer.Services\BlazorServer.Services.csproj" /> <ProjectReference Include="..\..\BlazorServer\BlazorServer.Models.Mappings\BlazorServer.Models.Mappings.csproj" /> </ItemGroup> </Project>
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true" } }
تعریف کنترلر HotelRoom
در ادامه کدهای اولین کنترلر Web API را مشاهده میکنید که مرتبط است با بازگشت اطلاعات تمام اتاقهای ثبت شده و یا بازگشت اطلاعات یک اتاق ثبت شده:
using BlazorServer.Models; using BlazorServer.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]")] public class HotelRoomController : ControllerBase { private readonly IHotelRoomService _hotelRoomService; public HotelRoomController(IHotelRoomService hotelRoomService) { _hotelRoomService = hotelRoomService; } [HttpGet] public IAsyncEnumerable<HotelRoomDTO> GetHotelRooms() { return _hotelRoomService.GetAllHotelRoomsAsync(); } [HttpGet("{roomId}")] public async Task<IActionResult> GetHotelRoom(int? roomId) { if (roomId == null) { return BadRequest(new ErrorModel { Title = "", ErrorMessage = "Invalid Room Id", StatusCode = StatusCodes.Status400BadRequest }); } var roomDetails = await _hotelRoomService.GetHotelRoomAsync(roomId.Value); if (roomDetails == null) { return BadRequest(new ErrorModel { Title = "", ErrorMessage = "Invalid Room Id", StatusCode = StatusCodes.Status404NotFound }); } return Ok(roomDetails); } } }
- ErrorModel آنرا در همان پروژهی قبلی مدلها، در فایل BlazorServer\BlazorServer.Models\ErrorModel.cs به صورت زیر ایجاد کردهایم:
namespace BlazorServer.Models { public class ErrorModel { public string Title { get; set; } public int StatusCode { get; set; } public string ErrorMessage { get; set; } } }
یکی از مزایای آن، امکان آزمایش API تهیه شده، بدون نیاز به تهیهی هیچ نوع کلاینت خاصی است. برای مثال اگر بر روی api/hotelroom آن کلیک کنیم، گزینهی «try it out» آن ظاهر شده و با کلیک بر روی آن، اینبار دکمهی execute ظاهر میشود. در ادامه با کلیک بر روی دکمهی اجرای آن، اکشن متد GetHotelRooms اجرا شده و خروجی زیر ظاهر میشود:
و یا اگر بخواهیم متد GetHotelRoom را توسط آن آزمایش کنیم، بر اساس پارامترهای آن، رابط کاربری زیر را تشکیل میدهد که امکان دریافت شمارهی اتاق را دارد:
انجام تنظیمات ابتدایی CORS و خروجی JSON برنامه
قرار است این API را از طریق پروژهی Blazor سمت کلاینت خود استفاده کنیم که آدرس آن، با آدرس API یکی نیست. به همین جهت نیاز است تنظیمات CORS را به صورت زیر اضافه کنیم:
namespace BlazorWasm.WebApi { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddCors(o => o.AddPolicy("HotelManagement", builder => { builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); })); services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; // To avoid `JsonSerializationException: Self referencing loop detected error` options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; }); // ... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseCors("HotelManagement"); app.UseRouting(); app.UseAuthentication(); // ... } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-24.zip
ابتدا از طریق فرمت protocol buffer، فایلهای خود را که قرار است انتقال داده شوند، مینویسیم.
سپس بصورت خودکار برای زبان برنامه نویسی مطبوع خود آن را generate میکنیم.
کدهای تولید شده بصورت خودکار و کاملا آماده هستند و ضمن اینکه encode/decode شدن بصورت خودکار توسط فریم ورک انجام شده و قابلیت تعامل بین زبانهای مختلف برنامه نویسی یا سرویسهای مختلف برقرار است.
نکته:
- بعضی از دیتابیسها از فرمت protocol buffers پشتیبانی میکنند.
- اکثر فریم ورکهای RPC شامل gRPC از پروتکل بافر برای تبادل دیتا استفاده میکنند.
- گوگل برای تمام سرویسهای داخلی خود از آن استفاده میکند.
- بعضی از پروژههای خیلی بزرگ مثل etcd از پروتکل بافر برای تبادل دیتا استفاده میکنند.
- ما در این مقاله از ورژن 3 پروتکل بافر استفاده میکنیم.
نصب Code generator
برای اینکه بتوانیم از طریق فایلهایی که میسازیم کدهای generate شده را تولید کنیم، احتیاج به کامپایلر مربوطه را داریم.
اگر از MacOSX استفاده میکنید، به راحتی با استفاده از دستور زیر میتوانید آن را نصب کنید:
brew install protobuf
اگر هم از ویندوز استفاده میکنید، از این طریق میتوانید نسخهی مورد نظر را به راحتی دانلود و مورد استفاده قرار بدهید:
https://github.com/google/protobuf/releases https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-win32.zip
حالا میخواهیم اولین فایل خود را با این فرمت بسازیم.
اول از همه با هم نگاهی به ساختار فایل مربوطه میاندازیم:
همانطور که در تصویر فوق میبینید، همه چیز به سادگی مشخص است؛ ورژن 3 که آخرین ورژن پروتکل بافر میباشد، آیتمی به نام MyMessage با پراپرتیهایی مشخص شده از Type بخصوص، تعریف شدهاند، تگها هم باید به ترتیب وارد شده باشند.
حالا میخواهیم بصورت واقعی protocol buffer خود را طراحی کرده و سپس از روی آن کدهای مربوطه را generate نماییم؛ به نام sample.proto بصورت زیر:
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
در فایل فوق علاوه بر تعریفهای اولیه، یک سرویس را هم اضافه کردهایم و همچنین متدی را با ورودی و خروجیهای مشخصی ایجاد کردهایم (امکانات پروتکل بافر خیلی بیشتر از این موارد است؛ از جمله فرمتهای آرایه و غیره را نیز پشتیبانی میکند، همچنین از روشی برای versioning استفاده میکند که obsolete کردن پراپرتیها و نسخه بندی را بسیار راحت میکند و ...). به سادگی قابلیت طراحی و پیاده سازی سرور و کلاینت مربوط به این آیتم ایجاد شده با استفاده از زبانهای برنامه نویسی مختلف فراهم میباشد. حال کافیاست که پروتکل بافر خود را با زبان دلخواه خود generate کنیم. در قسمت زیر برای زبانهای برنامه نویسی Go و #C، کدها را تولید میکنیم.
protoc sample.proto --go_out=plugins=grpc:.
protoc sample.proto --csharp_out=.
بعد از تولید شدن کدها با استفاده از زبان برنامه نویسی دلخواه خود میتوانید مشاهد کنید سرویس ها، تایپها و غیره همگی ساخته شدهاند و کاملا آمادهی استفاده هستند.
در مقالهی بعدی به آشنایی با gRPC میپردازیم و ضمن اینکه یک سرور با #C و یک کلاینت با زبان برنامه نویسی Go را نوشته که از طریق پروتکل بافر با هم به تبادل اطلاعات میپردازند!
نصب Swashbuckle (سوواَش باکِل)
اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بستهی مختلف مرتبط با آنرا خواهیم یافت. ما در این بین، بیشتر به این بستهها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائهی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامهی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائهی OpenAPI Specification استفاده میکنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شدهی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائهی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز میتوان استفاده کرد.
یک نکته: اگر صرفا بستهی Swashbuckle.AspNetCore را نصب کنیم، هر سه بستهی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آنها استفاده میکنید، انتخاب گزینهی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آنها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی میکند. خلاصهی این موارد، افزودن PackageReference زیر به فایل پروژهی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" /> </ItemGroup> </Project>
تنظیم میانافزار Swashbuckle
پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میانافزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه میکنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( name: "LibraryOpenAPISpecification", info: new Microsoft.OpenApi.Models.OpenApiInfo() { Title = "Library API", Version = "1", Description = "Through this API you can access authors and their books.", Contact = new Microsoft.OpenApi.Models.OpenApiContact() { Email = "name@site.com", Name = "DNT", Url = new Uri("https://www.dntips.ir") }, License = new Microsoft.OpenApi.Models.OpenApiLicense() { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }); }); }
اکنون در متد Configure، میانافزار آنرا خواهیم افزود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); // ... }
تا اینجا اگر برنامه را اجرا کنید، میتوان OpenAPI Specification تولیدی را در آدرس زیر یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.
نگاهی به OpenAPI Specification تولیدی
در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شدهی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهدهاست. سپس لیست مسیرهای این API مشخص شدهاند:
اینها مسیرهایی هستند که توسط دو کنترلر کتابها و نویسندگان برنامهی Web API ما عمومی شدهاند. در اینجا مقابل هر مسیر، تعداد آیتمهای متناظری نیز ذکر شدهاند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده:
که هر کدام به همراه نام متدها و پارامترهای متناظر با آنها نیز میشوند. به علاوه نوع responseهای پشتیبانی شدهی توسط این متدها نیز ذکر شدهاند. هر کدام از خروجیها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آنها بر اساس نوع مدلهای متناظر، استخراج و ارائه شدهاند.
مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست
اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آنرا صرفا 200 یا Ok ذکر کردهاست؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")] public async Task<ActionResult<Author>> GetAuthor(Guid authorId) { var authorFromRepo = await _authorsService.GetAuthorAsync(authorId); if (authorFromRepo == null) { return NotFound(); } return Ok(_mapper.Map<Author>(authorFromRepo)); }
افزودن و راه اندازی Swagger UI
در ادامه میخواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); }); // ... }
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:
و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آنها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شدهاست (از پارامترها تا نوع خروجی):
اکنون اگر بر روی دکمهی try it out آن نیز کلیک کنید، در همینجا میتوان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمهی execute که پس از کلیک بر روی دکمهی try it out ظاهر شدهاست، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.
در انتهای این صفحه، در قسمت schemas آن، مشخصات مدلهای بازگشت داده شدهی توسط Web API نیز ذکر شدهاند:
یک نکته: تغییر آدرس https://localhost:5001/swagger/index.html به ریشهی سایت
اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشهی سایت را در مسیر https://localhost:5001 باز میکنید، Swagger UI نمایان شود، میتوانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); setupAction.RoutePrefix = ""; });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-02.zip
در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.
NoSQL و مایکروسافت
1) Azure table storage
Azure table storage در حقیقت یک Key-value store ابری است و برای کار با آن از اینترفیس پروتکل استاندارد OData استفاده میشود. علت استفاده و طراحی یک سیستم Key-value store در اینجا، مناسب بودن اینگونه سیستمها جهت مقاصد عمومی است و به این ترتیب میتوان به بازه بیشتری از مصرف کنندگان، خدمات ارائه داد.
پیش از ارائه Azure table storage، مایکروسافت سرویس خاصی را به نام SQL Server Data Services که به آن SQL Azure نیز گفته میشود، معرفی کرد. این سرویس نیز یک Key-Value store است؛ هرچند از SQL Server به عنوان مخزن نگهداری اطلاعات آن استفاده میکند.
2) SQL Azure XML Columns
فیلدهای XML از سال 2005 به امکانات توکار SQL Server اضافه شدند و این نوع فیلدها، بسیاری از مزایای دنیای NoSQL را درون SQL Server رابطهای مهیا میسازند. برای مثال با تعریف یک فیلد به صورت XML، میتوان از هر ردیف به ردیفی دیگر، اطلاعات متفاوتی را ذخیره کرد؛ به این ترتیب امکان کار با یک فیلد که میتواند اطلاعات یک شیء را قبول کند و در حقیقت امکان تعریف اسکیمای پویا و متغیر را در کنار امکانات یک بانک اطلاعاتی رابطهای که از اسکیمای ثابت پشتیبانی میکند، میسر میشود. در این حالت در هر ردیف میتوان تعدادی ستون ثابت را با یک ستون XML با اسکیمای کاملا پویا ترکیب کرد.
همچنین SQL Server در این حالت قابلیتی را ارائه میدهد که در بسیاری از بانکهای اطلاعاتی NoSQL میسر نیست. در اینجا در صورت نیاز و لزوم میتوان اسکیمای کاملا مشخصی را به یک فیلد XML نیز انتساب داد؛ هر چند این مورد اختیاری است و میتوان یک un typed XML را نیز بکار برد. به علاوه امکانات کوئری گرفتن توکار از این اطلاعات را به کمک XPath ترکیب شده با T-SQL، نیز فراموش نکنید.
بنابراین اگر یکی از اهداف اصلی گرایش شما به سمت دنیای NoSQL، استفاده از امکان تعریف اطلاعاتی با اسکیمای متغیر و پویا است، فیلدهای نوع XML اس کیوال سرور را مدنظر داشته باشید.
یک مثال عملی: فناوری Azure Dev Fabric's Table Storage (نسخه Developer ویندوز Azure که روی ویندوزهای معمولی اجرا میشود؛ یک شبیه ساز خانگی) به کمک SQL Server و فیلدهای XML آن طراحی شده است.
3) SQL Azure Federations
در اینجا منظور از Federations در حقیقت همان پیاده سازی قابلیت Sharding بانکهای اطلاعاتی NoSQL توسط SQL Azure است که برای توزیع اطلاعات بر روی سرورهای مختلف طراحی شده است. به این ترتیب دو قابلیت Partitioning و همچنین Replication به صورت خودکار در دسترس خواهند بود. هر Partition در اینجا، یک SQL Azure کامل است. بنابراین چندین بانک اطلاعاتی فیزیکی، یک بانک اطلاعاتی کلی را تشکیل خواهند داد.
هرچند در اینجا Sharding (که به آن Federation member گفته میشود) و در پی آن مفهوم «عاقبت یک دست شدن اطلاعات» وجود دارد، اما درون یک Shard یا یک Federation member، مفهوم ACID پیاده سازی شده است. از این جهت که هر Shard واقعا یک بانک اطلاعاتی رابطهای است. اینجا است که مفهوم برنامههای Multi-tenancy را برای درک آن باید درنظر داشت. برای نمونه یک برنامه وب را درنظر بگیرید که قسمت اصلی اطلاعات کاربران آن بر روی یک Shard قرار دارد و سایر اطلاعات بر روی سایر Shards پراکنده شدهاند. در این حالت است که یک برنامه وب با وجود مفهوم ACID در یک Shard میتواند سریع پاسخ دهد که آیا کاربری پیشتر در سایت ثبت نام کرده است یا خیر و از ثبت نامهای غیرمجاز جلوگیری به عمل آورد.
در اینجا تنها موردی که پشتیبانی نشدهاست، کوئریهای Fan-out میباشد که پیشتر در مورد آن بحث شد. از این جهت که با نحوه خاصی که Sharding آن طراحی شده است، نیازی به تهیه کوئریهایی که به صورت موازی بر روی کلیه Shards برای جمع آوری اطلاعات اجرا میشوند، نیست. هر چند از هر shard با استفاده از برنامههای دات نت، میتوان به صورت جداگانه نیز کوئری گرفت.
4) OData
اگر به CouchDB و امکان دسترسی به امکانات آن از طریق وب دقت کنید، در محصولات مایکروسافت نیز این دسترسی REST API پیاده سازی شدهاند.
OData یک RESTful API است برای دسترسی به اطلاعاتی که به شکل XML یا JSON بازگشت داده میشوند. انواع و اقسام کلاینتهایی برای کار با آن از جاوا اسکریپت گرفته تا سیستمهای موبایل، دات نت و جاوا، وجود دارند. از این API نه فقط برای خواندن اطلاعات، بلکه برای ثبت و به روز رسانی دادهها نیز استفاده میشود. در سیستمهای جاری مایکروسافت، بسیاری از فناوریها، اطلاعات خود را به صورت OData دراختیار مصرف کنندگان قرار میدهند مانند Azure table storage، کار با SQL Azure از طریق WCF Data Services (جایی که OData از آن نشات گرفته شده)، Azure Data Market (برای ارائه فیدهایی از اطلاعات خصوصا رایگان)، ابزارهای گزارشگیری مانند SQL Server reporting services، لیستهای شیرپوینت و غیره.
به این ترتیب به بسیاری از قابلیتهای دنیای NoSQL مانند کار با اطلاعات JSON بدون ترک دنیای رابطهای میتوان دسترسی داشت.
5) امکان اجرای MongoDB و امثال آن روی سکوی کاری Azure
امکان توزیع MongoDB بر روی یک Worker role سکوی کاری Azure وجود دارد. در این حالت بانکهای اطلاعاتی این سیستمها بر روی Azure Blob Storage قرار میگیرند که به آنها Azure drive نیز گفته میشود. همین روش برای سایر بانکهای اطلاعاتی NoSQL نیز قابل اجرا است.
به علاوه امکان اجرای Hadoop نیز بر روی Azure وجود دارد. مایکروسافت به کمک شرکتی به نام HortonWorks نسخه ویندوزی Hadoop را توسعه دادهاند. HortonWorks را افرادی تشکیل دادهاند که پیشتر در شرکت یاهو بر روی پروژه Hadoop کار میکردهاند.
6) قابلیتهای فرا رابطهای SQL Server
الف) فیلدهای XML (که در ابتدای این مطلب به آن پرداخته شد). به این ترتیب میتوان به یک اسکیمای انعطاف پذیر، بدون از دست دادن ضمانت ACID رسید.
ب) فیلد HierarchyId برای ذخیره سازی اطلاعات چند سطحی. برای مثال در بانکهای اطلاعاتی NoSQL سندگرا، یک سند میتواند سند دیگری را در خود ذخیره کند و الی آخر.
ج) Sparse columns؛ ستونهای اسپارس تقریبا شبیه به Key-value stores عمل میکنند و یا حتی Wide column stores نیز با آن قابل مقایسه است. در اینجا هنوز اسکیما وجود دارد، اما برای نمونه علت استفاده از Wide column stores این نیست که واقعا نمیدانید ساختار دادههای مورد استفاده چیست، بلکه در این حالت میدانیم که در هر ردیف تنها از تعداد معدودی از فیلدها استفاده خواهیم کرد. به همین جهت در هر ردیف تمام فیلدها قرار نمیگیرند، چون در اینصورت تعدادی از آنها همواره خالی باقی میماندند. مایکروسافت این مشکل را با ستونهای اسپارس حل کرده است؛ در اینجا هر چند ساختار کلی مشخص است، اما مواردی که هر بار استفاده میشوند، تعداد محدودی میباشند. به این صورت SQL Server تنها برای ستونهای دارای مقدار، فضایی را اختصاص میدهد. به این ترتیب از لحاظ فیزیکی و ذخیره سازی نهایی، به همان مزیت Wide column stores خواهیم رسید.
د) FileStreams در اس کیوال سرور بسیار شبیه به پیوستهای سندهای بانکهای اطلاعاتی NoSQL سندگرا هستند. در اینجا نیز اطلاعات در فایل سیستم ذخیره میشوند اما ارجاعی به آنها در جداول مرتبط وجود خواهند داشت.
7) SQL Server Parallel Data Warehouse Edition
SQL PDW، نگارش خاصی از SQL Server است که در آن یک شبکه از SQL Serverها به صورت یک وهله منطقی SQL Server در اختیار برنامه نویسها قرار میگیرد.
این نگارش، از فناوری خاصی به نام MPP یا massively parallel processing برای پردازش کوئریها استفاده میکند. در اینجا همانند بانکهای اطلاعاتی NoSQL، یک کوئری به نود اصلی ارسال شده و به صورت موازی بر روی تمام نودها پردازش گردیده (همان مفهوم Map Reduce که پیشتر در مورد آن بحث شد) و نتیجه در اختیار مصرف کننده قرار خواهد گرفت. نکته مهم آن نیز در عدم نیاز به نوشتن کدی جهت رخ دادن این عملیات از طرف برنامه نویسها است و موتور پردازشی آن جزئی از سیستم اصلی است. تنها کافی است یک کوئری SQL صادر گردد تا نتیجه نهایی از تمام سرورها جمع آوری و بازگردانده شود.
این نگارش ویژه تنها به صورت یک Appliance به فروش میرسد (به صورت سخت افزار و نرم افزار باهم) که در آن CPUها، فضاهای ذخیره سازی اطلاعات و جزئیات شبکه به دقت از پیش تنظیم شدهاند.
تلاشهای بسیاری توسط توسعه گران صورت پذیرفته است تا فرایند ایجاد وب سرویس WCF در بستر HTTP آسان شود. امروزه وب سرویس هایی که از قالب REST استفاده میکنند مطرح هستند.
ASP.NET Web API از مفاهیم موجود در ASP.NET MVC مانند Controllerها استفاده میکند و بر مبنای آنها ساخته شده است. بدین شکل، توسعه گر میتواند با دانش موجود خود به سادگی وب سرویسهای مورد نظر را ایجاد کند. Web API، پروتوکل SOAP را به کتابهای تاریخی! سپرده است تا از آن به عنوان روشی برای تعامل بین سیستمها یاد شود. امروزه به دلیل فراگیری پروتوکل HTTP، بیشتر محیطهای برنامه نویسی و سیستم ها، از مبانی اولیهی پروتوکل HTTP مانند اَفعال آن پشتیبانی میکنند.
حال قصد داریم تا وب سرویسی را که در قسمت اول با WCF ایجاد کردیم، این بار با استفاده از Web API ایجاد کنیم. به تفاوت این دو دقت کنید.
using System.Web.Http; namespace MvcApplication1.Controllers { public class ValuesController : ApiController { // GET api/values/5 public string Get(int id) { return string.Format("You entered: {0}", id); } } }
نحوهی برگشت یک مقدار از متدها در Web API، مانند WCF است. میتوانید خروجی متد Get را با اجرای پروژهی قبل در Visual Studio و تست آن با یک مرورگر ملاحظه کنید. دقت داشته باشید که یکی از اصولی که Web API به آن معتقد است این است که وب سرویسها میتوانند ساده باشند. در Web API، تست و دیباگ وب سرویسها بسیار راحت است. با مرورگر Internet Explorer به آدرس http://localhost:{port}/api/values/3 بروید. پیش از آن، برنامهی Fiddler را اجرا کنید. شکل ذیل، نتیجه را نشان میدهد.
در اینجا نتیجه، عبارت "You entered: 3" است که به صورت یک متن ساده برگشت داده شده است.
ایجاد یک پروژهی Web API
در Visual Studio، مسیر ذیل را طی کنید.
File> New> Project> Installed Templates> Visual C#> Web> ASP.NET MVC 4 Web Application
نام پروژه را HelloWebAPI بگذارید و بر روی دکمهی OK کلیک کنید (شکل ذیل)در فرمی که باز میشود، گزینهی Web API را انتخاب و بر روی دکمهی OK کلیک کنید (شکل ذیل). البته دقت داشته باشید که ما همیشه مجبور به استفاده از قالب Web API برای ایجاد پروژههای خود نیستیم. میتوان در هر نوع پروژه ای از Web API استفاده کرد.
اضافه کردن مدل
مدل، شی ای است که نمایانگر دادهها در برنامه است. Web API میتواند به طور خودکار، مدل را به فرمت JSON، XML یا فرمت دلخواهی که خود میتوانید برای آن ایجاد کنید تبدیل و سپس دادههای تبدیل شده را در بدنهی پاسخ HTTP به Client ارسال کند. تا زمانی که Client بتواند فرمت دریافتی را بخواند، میتواند از آن استفاده کند. بیشتر Clientها میتوانند فرمت JSON یا XML را پردازش کنند. به علاوه، Client میتواند نوع فرمت درخواستی از Server را با تنظیم مقدار هدر Accept در درخواست ارسالی تعیین کند. اجازه بدهید کار خود را با ایجاد یک مدل ساده که نمایانگر یک محصول است آغاز کنیم.
بر روی پوشهی Models کلیک راست کرده و از منوی Add، گزینهی Class را انتخاب کنید.
نام کلاس را Product گذاشته و کدهای ذیل را در آن بنویسید.
namespace HelloWebAPI.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
مدل ما، چهار Property دارد که در کدهای قبل ملاحظه میکنید.
اضافه کردن Controller
در پروژه ای که با استفاده از قالب پیش فرض Web API ایجاد میشود، دو Controller نیز به طور خودکار در پروژهی Controller قرار میگیرند:
- HomeController: یک Controller معمول ASP.NET MVC است که ارتباطی با Web API ندارد.
- ValuesController: یک Controller مختص Web API است که به عنوان یک مثال در پروژه قرار داده میشود.
توجه: Controllerها در Web API بسیار شبیه به Controllerها در ASP.NET MVC هستند، با این تفاوت که به جای کلاس Controller، از کلاس ApiController ارث میبرند و بزرگترین تفاوتی که در نگاه اول در متدهای این نوع کلاسها به چشم میخورد این است که به جای برگشت Viewها، داده برگشت میدهند.
کلاس ValuesController را حذف و یک Controller به پروژه اضافه کنید. بدین منظور، بر روی پوشهی Controllers، کلیک راست کرده و از منوی Add، گزینهی Controller را انتخاب کنید.
توجه: در ASP.NET MVC 4 میتوانید بر روی هر پوشهی دلخواه در پروژه کلیک راست کرده و از منوی Add، گزینهی Controller را انتخاب کنید. پیشتر فقط با کلیک راست بر روی پوشهی Controller، این گزینه در دسترس بود. حال میتوان کلاسهای مرتبط با Controllerهای معمول را در یک پوشه و Controllerهای مربوط به قابلیت Web API را در پوشهی دیگری قرار داد.
نام Controller را ProductsController بگذارید، از قسمت Template، گزینهی Empty API Controller را انتخاب و بر روی دکمهی OK کلیک کنید (شکل ذیل).
فایلی با نام ProductsController.cs در پوشهی Controllers قرار میگیرد. آن را باز کنید و کدهای ذیل را در آن قرار دهید.
namespace HelloWebAPI.Controllers { using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using HelloWebAPI.Models; public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1.39M }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { var resp = new HttpResponseMessage(HttpStatusCode.NotFound); throw new HttpResponseException(resp); } return product; } public IEnumerable<Product> GetProductsByCategory(string category) { return products.Where( (p) => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } } }
برای ساده نگهداشتن مثال، لیستی از محصولات را در یک آرایه قرار داده ایم اما واضح است که در یک پروژهی واقعی، این لیست از پایگاه داده بازیابی میشود. در مورد کلاسهای HttpResponseMessage و HttpResponseException بعداً توضیح میدهیم.
در کدهای Controller قبل، سه متد تعریف شده اند:
- متد GetAllProducts که کل محصولات را در قالب نوع <IEnumerable<Product برگشت میدهد.
- متد GetProductById که یک محصول را با استفاده از مشخصهی آن (خصیصهی Id) برگشت میدهد.
- متد GetProductsByCategory که تمامی محصولات موجود در یک دستهی خاص را برگشت میدهد.
تمام شد! حال شما یک وب سرویس با استفاده از Web API ایجاد کرده اید. هر یک از متدهای قبل در Controller، به یک آدرس به شرح ذیل تناظر دارند.
GetAllProducts به api/products/
GetProductById به api/products/id/
GetProductsByCategory به api/products/?category=category/
در آدرسهای قبل، id و category، مقادیری هستند که همراه با آدرس وارد میشوند و در پارامترهای متناظر خود در متدهای مربوطه قرار میگیرند. یک Client میتواند هر یک از متدها را با ارسال یک درخواست از نوع GET اجرا کند.
در قسمت بعد، کار خود را با تست پروژه و نحوهی تعامل jQuery با آن ادامه میدهیم.
تنظیمات امنیتی دسترسی به سرور RavenDB
حالت پیش فرض دسترسی به سرور RavenDB
اگر فایل Raven.Server.exe.config را در یک ویرایشگر متنی باز کنید، یک چنین تنظیماتی در آن قابل مشاهده هستند:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="Raven/Port" value="*"/> <add key="Raven/DataDir" value="~\Database\System"/> <add key="Raven/AnonymousAccess" value="Admin"/> </appSettings> <runtime> <loadFromRemoteSources enabled="true"/> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="Analyzers;Plugins"/> </assemblyBinding> </runtime> </configuration>
حالت پیش فرض دسترسی به RavenDB برای کاربران اعتبارسنجی نشده، حالت Get است (خواندن اطلاعات) و هیچگونه دسترسی تغییر اطلاعات آنرا ندارند (حالت Read only). اگر این کلید به All تنظیم شود، کلیه کاربران، قابلیت Read و Write را خواهند داشت. حالت None به این معنا است که تنها کاربران اعتبارسنجی شده میتوانند به دیتابیس دسترسی پیدا کنند.
اگر علاقمند هستید که مجوزهای یک کاربر متصل را مشاهده کنید، از فرمان ذیل استفاده نمائید:
var json = ((ServerClient) store.DatabaseCommands).CreateRequest("GET", "/debug/user-info").ReadResponseJson();
نکته بسیار مهم
اگر مجوز RavenDB را نخریده باشید، مقدار Admin تنها مقداری است که در اینجا میتوانید تنظیم کنید. به این معنا که کلیه کاربران، دسترسی Admin را به سرور خواهند داشت. (و بدیهی است فقط برای آزمایش سیستم مناسب است)
سعی در تنظیم حالت اعتبار سنجی زمانیکه از مجوز AGPL استفاده میکنید، با یک استثناء از طرف سرور متوقف خواهد شد.
Windows authentication
اعتبار سنجی پیش فرض مورد استفاده نیز Windows authentication است. به این معنا که تنها کاربری با دارا بودن اکانت معتبری بر روی سیستم و یا دومین ویندوزی، امکان کار با RavenDB را خواهد داشت. در این حالت کلیه کاربران دومین به سرور دسترسی خواهند داشت. اگر این حالت مطلوب شما نیست، میتوان از گروههای ویژه کاربران تعریف شده بر روی سیستم و یا بر روی دومین ویندوزی استفاده کرد.
این تنظیمات باید بر روی دیتابیس System صورت گیرند، در قسمت Settings و حالت Windows authentication :
اعتبارسنجی OAuth
شاید دسترسی به سرور RavenDB همیشه از طریق Windows authentication مطلوب نباشد. برای این حالت از روش اعتبارسنجی سفارشی خاصی به نام OAuth نیز پشتیبانی میشود. این حالت به صورت توکار در سرور RavenDB پیاده سازی شده است و یا میتوان با پیاده سازی اینترفیس IAuthenticateClient کنترل بیشتری را اعمال کرد. البته با دریافت افزونه Raven.Bundles.Authentication به یک نمونه پیاده سازی شده آن دسترسی خواهید داشت. پس از دریافت آن، فایل اسمبلی مربوطه را به درون پوشه افزونههای سرور کپی کنید تا آماده استفاده شود.
PM> Install-Package RavenDB.Bundles.Authentication -Pre
فایل کانفیگ سرور را برای افزودن سطر ذیل ویرایش کنید:
<add key="Raven/AuthenticationMode" value="OAuth"/>
var documentStore = new DocumentStore { ApiKey = "sample/ThisIsMySecret", Url = "http://localhost:8080/" };
$("#JQGrid1").jqGrid({ url: "/manager/Products/OnProductDataRequested", editurl: '/manager/Products/EditProductData', mtype: "GET", datatype: "json", page: 1, sortname: 'Priority', sortorder: "desc", viewrecords: true, jsonReader: { id: "Id" }, prmNames: { id: "Id" }, colNames: ["Id","خلاصه","توضیح خلاصه","قیمت","قیمت با تخفیف","درصد کارمزد سایت","آستانه هشدار موجودی","محتوا", "عنوان","فروشگاه","گروه","تعداد موجودی", "اولویت","محصولات مکمل","تصاویر","مقادیر مشخصه محصول","قیمت","نظرات", "فعال","ویژه","موجود","برند","عملیات درجا","عملیات کامل"], colModel: [ { key: true, width: 50, name: "Id", hidden: true, search: false }, { editable: true, width: 10, name: "Abstract", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "AbstractDescription", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Value", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Discount", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "SiteWagePercentage", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "InventoryAlertLimit", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Context", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 150, name: "Title", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { name: "shopTitle", align: 'center', viewable: true, editrules: { edithidden: true }, search: true, editable: true, stype: 'select', edittype: 'select', searchoptions: { sopt: ["eq", "ne"], dataUrl: "/manager/Products/Getshop/", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.shId + '">' + this.shTitle + '</option>'; }); } return s + '</select>'; }, }, editoptions: { dataUrl: "/manager/Products/Getshop", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.shId + '">' + this.shTitle + '</option>'; }); } return s + '</select>'; }, } }, { editable: true, name: "groupTitle", search: true, stype: "select" , editrules: { edithidden: true }, search: true, edittype: 'select', editoptions: { dataUrl: "/manager/Products/GetGroups", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.grpId + '">' + this.grpTitle + '</option>'; }); } return s + '</select>'; }, } }, { editable: true, width: 70, name: "AvailableCount", search: true, stype: "number", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: true, width: 50, name: "Priority", search: true, stype: "number", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "ComplementProducts", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 70, name: "Images", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 100, name: "ProductProperty", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "Price", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "Comments", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: true, width: 50, name: "Active", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" } , formatoptions: { disabled: false} }, { editable: true, width: 80, name: "AmazingOffer", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" } , formatoptions: { disabled: false} }, { editable: true, width: 80, name: "Available", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" }, searchoptions: { "sopt": ["bw", "eq"] }, formatoptions: { disabled: false } }, { name: 'BrandId', align: 'center', hidden: true, viewable: true, editrules: { edithidden: true }, editable: true, stype: 'select', edittype: 'select', editoptions: { dataUrl: "/manager/Products/GetBrands", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.brandId + '">' + this.brandTitle + '</option>'; }); } return s + '</select>'; }, } }, { name: "myac", width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true } }, { editable: false, width: 70, name: "FullEdit", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, // BLAH, BLAH, BLAH ], gridComplete: function () { var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); for (var i = 0; i < ids.length; i++) { var cl = ids[i]; ComplementProducts = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ComplementProducts/Index/" + cl + " }) + '><span class=\"fa fa-shopping-cart\" style='color: white;'></span></a></div>"; Images = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Images/Index/" + cl + " }) + '><span class=\"fa fa-picture-o\" style='color: white;'></span></a></div>"; ProductProperty = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ProductPropertyItems/Index/" + cl + " }) + '><span class=\"fa fa-braille\" style='color: white;'></span></a></div>"; Price = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/pricesppitems/Index/" + cl + " }) + '><span class=\"fa fa-line-chart\" style='color: white;'></span></a></div>"; Comments = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Comments/" + cl + " }) + '><span class=\"fa fa-comments\" style='color: white;'></span></a></div>"; FullEdit = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Edit/" + cl + " }) + '><span class=\"fa fa-edit\" style='color: white;'></span></a><a class='btn btn-primary' href=/manager/Products/Details/" + cl + " }) + '><span class=\"fa fa-exclamation-circle\" style='color: white;'></span></a></div>"; jQuery("#JQGrid1").jqGrid('setRowData', ids[i], { ComplementProducts: ComplementProducts, Images: Images, ProductProperty: ProductProperty, Price: Price, Comments: Comments, FullEdit: FullEdit}); } }, loadComplete: function () { var activeButton = getColumnIndexByName('Active'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (activeButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); // alert("active clicked " + rowId); $.ajax({ type: "POST", url: "/manager/Products/Activate", data: { id: rowId }, dataType: "json" }); }); var amazingOfferButton = getColumnIndexByName('AmazingOffer'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (amazingOfferButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); $.ajax({ type: "POST", url: "/manager/Products/ShowInAmazingOffer", data: { id: rowId }, dataType: "json" }); $('#JQGrid1').trigger('reloadGrid'); }); var availableButton = getColumnIndexByName('Available'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (availableButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); $.ajax({ type: "POST", url: "/manager/Products/Available", data: { id: rowId }, dataType: "json" }); $('#JQGrid1').trigger('reloadGrid'); }); }, height: "auto", caption: "", viewrecords: true, rowNum: 20, direction: "rtl", pager: jQuery('#JQGrid1_pager'), rowList: [10, 20, 30, 40], toppager: true, jsonReader: { root: "rows", page: "page", total: "total", records: "records", repeatitems: false, Id: "0" }, }).jqGrid('navGrid', '#JQGrid1_pager', // the buttons to appear on the toolbar of the grid { edit: true, add: true, del: true, search: true, refresh: true, view: false, position: "left", cloneToTop: true,searchtext:"جستجو" }, // options for the Edit Dialog { width: 450, editCaption: "ویرایش محصول", recreateForm: true, closeAfterEdit: true, viewPagerButtons: false, //afterShowForm: populateGroups, errorTextFormat: function (data) { return 'Error: ' + data.responseText } }, // Add options { url: '/TabMaster/Create', closeAfterAdd: true }, // Delete options { url: '/manager/Products/Remove' }, { zIndex: 100, caption: "جستجوی محصول", sopt: ['cn'] } );