var id = Page.RouteData.Values["id"]; var title = Page.RouteData.Values["title"]; var blogPost = _postsService.FindPost(id); if(blogPost == null) { Response.RedirectPermanent("~/notfound"); } if(blogPost.Title != title) { Response.RedirectPermanent("~/post/" + id + "/" + blogPost.Title); }
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
مدل و منبع داده برنامه
namespace jQueryMvcSample06.Models { public class BlogPost { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
using System.Collections.Generic; using jQueryMvcSample06.Models; namespace jQueryMvcSample06.DataSource { /// <summary> /// منبع داده فرضی جهت سهولت دموی برنامه /// </summary> public static class BlogPostDataSource { private static IList<BlogPost> _cachedItems; static BlogPostDataSource() { _cachedItems = createBlogPostsInMemoryDataSource(); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogPost> createBlogPostsInMemoryDataSource() { var results = new List<BlogPost>(); for (int i = 1; i < 30; i++) { results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i}); } return results; } public static IList<BlogPost> LatestBlogPosts { get { return _cachedItems; } } } }
کنترلر برنامه
using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample06.DataSource; using jQueryMvcSample06.Security; namespace jQueryMvcSample06.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { var postsList = BlogPostDataSource.LatestBlogPosts; return View(postsList); } [AjaxOnly] [HttpPost] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public ActionResult DeleteRow(int? postId) { if (postId == null) return Content(null); //todo: delete post from db return Content("ok"); } } }
data: JSON.stringify({ postId: postId }),
View برنامه
@model IEnumerable<jQueryMvcSample06.Models.BlogPost> @{ ViewBag.Title = "Index"; var postUrl = Url.Action(actionName: "DeleteRow", controllerName: "Home"); } <h2> حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن</h2> <table> <tr> <th> عملیات </th> <th> عنوان </th> </tr> @foreach (var item in Model) { <tr> <td> <span id="row-@item.Id">حذف</span> </td> <td> @item.Title </td> </tr> } </table> @section JavaScript { <script type="text/javascript"> $(function () { $('span[id^="row"]').click(function () { var span = $(this); var postId = span.attr('id').replace('row-', ''); var tableRow = span.parent().parent(); $.ajax({ type: "POST", url: '@postUrl', data: JSON.stringify({ postId: postId }), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = "/login"; } else if (status === 'error' || !data || data == "nok") { alert('خطایی رخ داده است'); } else { $(tableRow).fadeTo(600, 0, function () { $(tableRow).remove(); }); } } }); }); }); </script> }
در کدهای اسکریپتی صفحه، ابتدا کلیک بر روی کلیه spanهایی که id آنها با row شروع میشود را مونیتور خواهیم کرد:
$('span[id^="row"]').click(function () {
var span = $(this); var postId = span.attr('id').replace('row-', ''); var tableRow = span.parent().parent();
دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample06.zip
زمانیکه نیاز است موجودیت A با هیچ و یا حداکثر یک وهله از موجودیت B در ارتباط باشد، به یک چنین رابطهای One-to-Zero-or-One میگویند. برای اینکه یک چنین ارتباطی را تشکیل دهیم، نیاز است کلید اصلی یک جدول، در جدول مرتبط به آن، هم به عنوان کلید اصلی و هم به عنوان کلید خارجی معرفی شود؛ همانند شکل زیر که در آن CartableId، همزمان به صورت PK و FK تعریف شدهاست که به آن one-to-one association with shared primary key نیز میگویند:
الف) مدلسازی رابطهی One-to-Zero-or-One توسط Fluent API
در اینجا دو موجودیت را ملاحظه میکنید که توسط دو navigation property به هم متصل شدهاند:
public class UserProfile { public int UserProfileId { get; set; } public string UserName { get; set; } public virtual Cartable Cartable { get; set; } }
public class Cartable { public int CartableId { get; set; } public virtual UserProfile UserProfile { get; set; } }
public class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Cartable>(entity => { entity.Property(e => e.CartableId).ValueGeneratedNever(); entity.HasOne(d => d.UserProfile) .WithOne(p => p.Cartable) .HasForeignKey<Cartable>(d => d.CartableId); }); } public virtual DbSet<Cartable> Cartables { get; set; } public virtual DbSet<UserProfile> UserProfiles { get; set; } }
همچنین بر روی CartableId، فراخوانی متد ValueGeneratedNever را مشاهده میکنید. این متد را در قسمت «روشهای مختلف تولید خودکار مقادیر خواص» مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکسها» پیشتر بررسی کردیم. هدف آن این است که به بانک اطلاعاتی اعلام کند، این فیلد یک کلید اصلی از نوع خود افزایش یابنده نیست و مقدار آن توسط برنامه به صورت صریح تنظیم میشود (چون کلید خارجی نیز هست و به کلید اصلی جدول سمت دیگر رابطه اشاره میکند).
ب) مدلسازی رابطهی One-to-Zero-or-One توسط Data Annotations
برای تنظیم این رابطه توسط ویژگیها نیاز است DatabaseGenerated به None تنظیم شود تا کلید اصلی CartableId به صورت خودکار توسط بانک اطلاعاتی تولید نشود. همچنین این کلید اصلی نیز باید به صورت کلید خارجی نیز معرفی شود. به علاوه توسط InversePropertyها، باید هر دو سطر به هم مرتبط، ذکر شوند:
public class Cartable { [DatabaseGenerated(DatabaseGeneratedOption.None)] public int CartableId { get; set; } [ForeignKey("CartableId")] [InverseProperty("Cartable")] public virtual UserProfile UserProfile { get; set; } }
public class UserProfile { public int UserProfileId { get; set; } public string UserName { get; set; } [InverseProperty("UserProfile")] public virtual Cartable Cartable { get; set; } }
بررسی رابطهی One-to-One
تشکیل رابطهی One-to-One که در آن برخلاف رابطهی One-to-Zero-or-One، وجود هر دو سر رابطه اجباری هستند، در SQL Server میسر نیست (زیرا زمانیکه یک چنین رابطهای تشکیل شود، نمیتوان رکوردی را Insert کرد! چون زمانیکه هنوز یک سر رابطه ثبت نشدهاست، چگونه میتوان Id آنرا در سر دیگری به اجبار ثبت کرد؟!). SQL Server این رابطه را نیز به صورت همان One-to-Zero-or-One تشکیل میدهد. تنظیمات آن نیز با قبل تفاوتی ندارد. در این حالت اجباری بودن و یا نبودن یک سر رابطه همانند نکات قسمت «تعیین اجباری بودن یا نبودن ستونها در EF Core» در مطلب «شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوعهای داده و ویژگیهای آنها» است و این تنظیمات در اینجا صرفا از دیدگاه EF Core مفهوم دارند.
جهت تکمیل بحث، روش تشکیل رابطهی One-to-One بدون استفاده از روش به اشتراک گذاری کلید اصلی (one-to-one association with shared primary key) به صورت زیر است:
همانطور که مشاهده میکنید، در اینجا هر بلاگ حداکثر یک تصویر را میتواند داشته باشد. علت آن نیز به ذکر MyBlogForeignKey بر میگردد که یک کلید خارجی نال نپذیر است.
public class MyBlog { public int MyBlogId { get; set; } public string Url { get; set; } public MyBlogImage MyBlogImage { get; set; } } public class MyBlogImage { public int MyBlogImageId { get; set; } public byte[] Image { get; set; } public string Caption { get; set; } public int MyBlogForeignKey { get; set; } public MyBlog MyBlog { get; set; } }
modelBuilder.Entity<MyBlog>() .HasOne(p => p.MyBlogImage) .WithOne(i => i.MyBlog) .HasForeignKey<MyBlogImage>(b => b.MyBlogForeignKey);
تغییر نوع DbContext برنامه
پیش از شروع به یکپارچه کردن ASP.NET Core Identity با برنامهی جاری، نیاز است نوع DbContext آنرا به صورت زیر تغییر داد:
using BlazorServer.Entities; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace BlazorServer.DataAccess { public class ApplicationDbContext : IdentityDbContext { // ...
- این تغییر، نیاز به نصب بستهی نیوگت Microsoft.AspNetCore.Identity.EntityFrameworkCore را نیز در پروژهی جاری دارد تا IdentityDbContext آن شناسایی شده و قابل استفاده شود.
نصب ابزار تولید کدهای ASP.NET Core Identity
اگر از ویژوال استودیوی کامل استفاده میکنید، گزینهی افزودن کدهای ASP.NET Core Identity به صورت زیر قابل دسترسی است:
project -> right-click > Add > New Scaffolded Item -> select Identity > Add
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.AspNetCore.Identity.UI dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet aspnet-codegenerator identity --dbContext BlazorServer.DataAccess.ApplicationDbContext --force
حال اگر به پروژه دقت کنیم، پوشهی جدید Areas که به همراه فایلهای مدیریتی ASP.NET Core Identity است، اضافه شده و حاوی کدهای صفحات لاگین، ثبت نام کاربر و غیره است.
اعمال تغییرات ابتدایی مورد نیاز جهت استفاده از ASP.NET Core Identity
تا اینجا کدهای پیشفرض مدیریتی ASP.NET Core Identity را به پروژه اضافه کردیم. در ادامه نیاز است تغییرات ذیل را به پروژهی اصلی Blazor Server اعمال کنیم تا بتوان از این فایلها استفاده کرد:
- به فایل BlazorServer.App\Startup.cs مراجعه کرده و UseAuthentication و UseAuthorization را دقیقا در محلی که مشاهده میکنید، اضافه میکنیم. همچنین در اینجا نیاز است مسیریابیهای razor pages را نیز فعال کرد.
namespace BlazorServer.App { public class Startup { // ... public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); // ... }); } } }
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../BlazorServer.App/ add AddIdentity --context ApplicationDbContext dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
افزودن گزینهی منوی لاگین به برنامهی Blazor Server
پس از این تغییرات، به برنامهای رسیدهایم که مدیریت قسمت Identity آن، توسط قالب استاندارد مایکروسافت که در پوشهی Areas\Identity\Pages\Account نصب شده و بر اساس فناوری ASP.NET Core Razor Pages کار میکند، انجام میشود.
اکنون میخواهیم در منوی برنامهی Blazor Server خود که با صفحات Identity یکی شدهاست، لینکی را به صفحهی لاگین این Area اضافه کنیم. اگر به فایل Shared\MainLayout.razor آن مراجعه کنیم، به صورت پیشفرض، لینکی به صفحهی About، قرار دارد. به همین جهت این مورد را به صورت زیر اصلاح میکنیم:
ابتدا کامپوننت جدید BlazorServer.App\Shared\LoginDisplay.razor را با محتوای زیر ایجاد میکنیم:
<a href="Identity/Account/Register">Register</a> <a href="Identity/Account/Login">Login</a> @code { }
سپس از این کامپوننت در فایل BlazorServer.App\Shared\MainLayout.razor استفاده میکنیم:
<div class="top-row px-4"> <LoginDisplay></LoginDisplay> <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a> </div>
ثبت و فعالسازی سرویسهای کار با ASP.NET Core Identity
البته اگر در این حال برنامه را اجرا کنیم، با کلیک بر روی لینکهای فوق، استثنائی را مانند یافت نشدن سرویس UserManager، مشاهده خواهیم کرد. برای رفع این مشکل، به فایل BlazorServer.App\Startup.cs مراجعه کرده و سرویسهای Identity را ثبت میکنیم:
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders() .AddDefaultUI(); // ...
همانطور که مشاهده میکنید، قالب این قسمت Identity، با قالب قسمت Blazor Server یکی نیست؛ چون توسط Razor Pages و Area آن تامین میشود که master page خاص خودش را دارد. زمانیکه قالب Identity را اضافه میکنیم، علاوه بر Area خاص خودش، پوشهی جدید Pages\Shared را نیز ایجاد میکند که قالب صفحات Identity را به کمک فایل Pages\Shared\_Layout.cshtml تامین میکند:
بنابراین سفارشی سازی قالب این قسمت، شبیه به قالبی که برای کامپوننتهای Blazor مورد استفاده قرار میگیرد، باید در اینجا انجام شود و سفارشی سازی قالب کامپوننتهای Blazor، در پوشهی Shared ای که در ریشهی پروژهاست (BlazorServer.App\Shared\MainLayout.razor) انجام میشود.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-21.zip
تلاشهای بسیاری توسط توسعه گران صورت پذیرفته است تا فرایند ایجاد وب سرویس 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 با آن ادامه میدهیم.
در ادامه قواعد زیر را در نظر بگیرید :
1 - برای استفاده از یک کتابخانه خارجی (dll) در داخل کد Text Template از بلوک <#@ assembly #> استفاده میشود .
مثلا برای استفاده از System.xml کد <#@ assembly name="System.xml" #> رو قرار بدید .
2- برای import کردن یک فضای نام نیز میتوانید از بلوک #> <#@ استفاده نمایید .
به عنوان مثال : <#@ import namespace="System.Data" #>
3- encoding خروجی قابل تظیم میباشد. مثال: <#@ " output extension = ".html" encoding = "utf-8 #>
4- برای استفاده از یک فایل tt درون فایل دیگر میتوان از include استفاده کرد. مثال : <#@ " include file=" testpath \basetest.tt #>
حالا به مثال زیر توجه کنید:
در بسیاری از پروژهها، معادل تمامی جداول موجود در یک دیتابیس Class هایی به نام DTO یا Data transfer object ساخته میشود که عموما" کلاسهای سبکی هستند که فقط شامل خصوصیتهای معادل فیلدهای جداول میباشند و از آنها جهت مدل کردن دادهها و ... استفاده میگردد. تولید این کلاسهای ساده میتواند بصورت اتوماتیک صورت گیرد و از این جهت در زمان تولید پروژه صرفه جویی شود. همچنین با تغییر ساختار دیتابیس میتوان همواره کلاسها را بروزرسانی کرد. نمونه ای از کد T4 برای تولید تمامی کلاسهای DTO به شکل زیر میباشد . فقط برای تست کد زیر دقت کنید که ConnectionString را در داخل کد متناسب با دیتابیس خود تغییر دهید:
<#@ template language="C#" debug="True" hostspecific="True" #> <#@ output extension=".cs" #> <#@ assembly name="System.Data" #> <#@ assembly name="System.xml" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Data.SqlClient" #> <#@ import namespace="System.Data" #> using System; namespace MyProject.Entities { <# string connectionString = "Password=22125110;Persist Security Info=True;User ID=sa;Initial Catalog=DnnDB;Data Source=ABBASPOOR299"; SqlConnection conn = new SqlConnection(connectionString); conn.Open(); System.Data.DataTable schema = conn.GetSchema("TABLES"); string selectQuery = "select * from @tableName"; SqlCommand command = new SqlCommand(selectQuery,conn); SqlDataAdapter ad = new SqlDataAdapter(command); System.Data.DataSet ds = new DataSet(); foreach(System.Data.DataRow row in schema.Rows) { #> public class <#= row["TABLE_NAME"].ToString().Trim('s') #> { <# command.CommandText = selectQuery.Replace("@tableName",row["TABLE_NAME"].ToString()); ad.FillSchema(ds, SchemaType.Mapped, row["TABLE_NAME"].ToString()); foreach (DataColumn dc in ds.Tables[row["TABLE_NAME"].ToString()].Columns) { #> private <#= dc.DataType.Name #> _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower()) #>; public <#= dc.DataType.Name #> <#= dc.ColumnName #> { get { return _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower()) #>; } set { _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower()) #> = value; } } <# } #> } <# } #> }
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»
اما EF Core نه تنها این مشکل را پوشش را دادهاست، بلکه امکان تزریق وابستگیها و استفادهی از سرویسهای مختلف را نیز در این حین، پیش بینی کردهاست که در ادامه جزئیات آنرا مرور میکنیم.
نیاز به تغییر رشتهی اتصالی به بانک اطلاعاتی در زمان اجرا
دلایل نیاز به امکان تغییر رشتهی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامههایی کمی پیچیدهتر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کردهاند، متصل شده و از آن استفاده کنند.
- یکی از روشهای پیاده سازی برنامههای چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامهی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانکهای اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite
روش تغییر رشتهی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection") ));
یک نکته: چون این اطلاعات کش نمیشوند، اگر رشتهی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آنرا به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجهی محاسبه شدهی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.
دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیدهی به آن
فرض کنید قالب رشتهی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": { "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True", }
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژهی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه میتوان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویسهای جاری سیستم را میسر میکند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) => { var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate"); var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First(); var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName); dbContextBuilder.UseSqlServer(connectionString); });
همچنین در صورت نیاز میتوان UseSqlServer آنرا نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.
یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش میتواند تبدیل به یک نقیصهی امنیتی شود. به همین جهت برای مثال میتوان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.
connection.Insert(new Car { Name = "Volvo" });
connection.Update(new Car() { Id = 1, Name = "Saab" });
connection.Delete(new Car() { Id = 1 });
آپدیت 2 ویژوال استودیو 2015
بعد از نصب به روز رسانی دوم، اگر به اینترنت متصل نباشید، این خطاها را دریافت خواهید کرد:
Visual Studio Update 2 Extensibility Item Templates with Assembly References in Nuget Packages : This product did not download successfully: HTTP status 502: The server, while acting as a gateway or proxy to fulfill the request, received an invalid response from the upstream server it accessed. Visual Studio 2015 Software Development Kit Update 2 : This product did not download successfully: HTTP status 502: The server, while acting as a gateway or proxy to fulfill the request, received an invalid response from the upstream server it accessed. Developer Analytics Tools v5.2.0 : This product did not download successfully: HTTP status 502: The server, while acting as a gateway or proxy to fulfill the request, received an invalid response from the upstream server it accessed. JavaScript Language Service for Visual Studio : This product did not download successfully: HTTP status 502: The server, while acting as a gateway or proxy to fulfill the request, received an invalid response from the upstream server it accessed. JavaScript Project System for Visual Studio : This product did not download successfully: HTTP status 502: The server, while acting as a gateway or proxy to fulfill the request, received an invalid response from the upstream server it accessed.
لینکهای مستقیم دریافت این بستهها
Visual Studio Update 2 Extensibility Item Templates with Assembly References in Nuget Packages
Visual Studio 2015 Software Development Kit Update 2
Developer Analytics Tools v5.2.0
JavaScript Language Service for Visual Studio
JavaScript Project System for Visual Studio
روش یافتن این لینکها هم به صورت زیر است:
الف) فایل XML مربوط به فید به روز رسانی دوم را دریافت کنید:
http://go.microsoft.com/fwlink/?LinkID=646969
ب) سپس عنوان مواردی را که ذکر شده از اینترنت دریافت نشدهاند، در این فایل جستجو کنید. لینکی که در قسمت id هر کدام ذکر شده، دقیقا لینک دانلود آفلاین آن است.
زمانی که سیستم عامل های GUI مثل ویندوز به بازار آمدند، یکی از قسمتهای گرافیکی آنها AddressBar نام داشت که مسیر حرکت آنها را در فایل سیستم نشان میداد و در سیستم عاملهای متنی CLI با دستور cd یا pwd انجام میشد. بعدها در وب هم همین حرکت با نام BreadCrumb صورت گرفت که به عنوان مثال مسیر رسیدن به صفحهی یک محصول یا یک مقاله را نشان میداد. در یک پروژهی اندرویدی نیاز بود تا یک ساختار درختی را پیاده سازی کنم، ولی در برنامههای اندروید ایجاد یک درخت، کار هوشمندانه و مطلوبی نیست و روش کار به این صورت است که یک لیست از گروههای والد را نمایش داده و با انتخاب هر آیتم لیست به آیتمهای فرزند تغییر میکند. حالا مسئله این بود که کاربر باید مسیر حرکت خودش را بشناسد. به همین علت مجبور شدم یک BreadCrumb را برای آن طراحی کنم که در زیر تصویر آن را مشاهده میکنید.
از نکات جالب توجه در مورد این ماژول میتوان گفت که قابلیت این را دارد تا تصمیمات خود را بر اساس اندازههای مختلف صفحه نمایش بگیرد. به عنوان مثال اگر آیتمهای بالا بیشتر از سه عدد باشد و در صفحه جا نشود از یک مسیر جعلی استفاده میکند و همهی آیتمها با اندیس شماره 1 تا index-3 را درون یک آیتم با عنوان (...) قرار میدهد که من به آن میگویم مسیر جعلی. به عنوان نمونه مسیر تصویر بالا در صفحه جا شده است و نیازی به این کار دیده نشده است. ولی تصویر زیر از آن جا که مسیر، طول width صفحه نمایش رد کرده است، نیاز است تا چنین کاری انجام شود. موقعیکه کاربر آیتم ... را کلیک کند، مسیر باز شده و به محل index-3 حرکت میکند. یعنی دو مرحله به عقب باز میگردد.
نگاهی به کارکرد ماژول
قبل از توضیح در مورد سورس، اجازه دهید نحوهی استفاده از آن را ببینیم.
این سورس شامل دو کلاس است که سادهترین کلاس آن AndBreadCrumbItem میباشد که مشابه کلاس ListItem در بخش وب دات نت است و دو مقدار، یکی متن و دیگری Id را میگیرد:
سورس:
public class AndBreadCrumbItem { private int Id; private String diplayText; public AndBreadCrumbItem(int Id, String displayText) { this.Id=Id; this.diplayText=displayText; } public String getDiplayText() { return diplayText; } public void setDiplayText(String diplayText) { this.diplayText = diplayText; } public int getId() { return Id; } public void setId(int id) { Id = id; } }
به عنوان مثال میخواهیم یک breadcrumb را با مشخصات زیر بسازیم:
AndBreadCrumbItem itemhome=new AndBreadCrumbItem(0,"Home"); AndBreadCrumbItem itemproducts=new AndBreadCrumbItem(12,"Products"); AndBreadCrumbItem itemdigital=new AndBreadCrumbItem(15,"Digital"); AndBreadCrumbItem itemhdd=new AndBreadCrumbItem(56,"Hard Disk Drive");
AndBreadCrumb breadCrumb=new AndBreadCrumb(this); breadCrumb.AddNewItem(itemhome); breadCrumb.AddNewItem(itemproducts); breadCrumb.AddNewItem(itemdigital); breadCrumb.AddNewItem(itemhdd);
breadCumb.setContext(this);
پس از افزودن آیتم ها، تنظیمات زیر را اعمال نمایید:
LinearLayout layout=(LinearLayout)getActivity().findViewById(R.id.breadcumblayout); layout.setPadding(8, 8, 8, 8); breadCrumb.setLayout(layout); breadCrumb.SetTinyNextNodeImage(R.drawable.arrow); breadCrumb.setTextSize(25); breadCrumb.SetViewStyleId(R.drawable.list_item_style);
حال برای رسم آن متد UpdatePath را صدا میزنیم:
breadCrumb.UpdatePath();
الان اگر برنامه اجرا شود باید breadcrumb از چپ به راست رسم گردد. برای استفادههای فارسی، راست به چپ میتوانید از متد زیر استفاده کنید:
breadCrumb.setRTL(true);
در صورتیکه قصد دارید تنظیمات بیشتری چون رنگ متن، فونت متن و ... را روی هر المان اعمال کنید، از رویداد زیر استفاده کنید:
breadCrumb.setOnTextViewUpdate(new ITextViewUpdate() { @Override public TextView UpdateTextView(Context context, TextView tv) { tv.setTextColor(...); tv.setTypeface(...); return tv; } });
همچنین در صورتیکه میخواهید بدانید کاربر بر روی چه عنصری کلیک کرده است، از رویداد زیر استفاده کنید:
breadCumb.setOnClickListener(new IClickListener() { @Override public void onClick(int position, int Id) { //... } });
آخرین متد موجود که کمترین استفاده را دارد، متد SetNoResize است. در صورتیکه این متد با True مقداردهی گردد، عملیات تنظیم بر اساس صفحهی نمایش لغو میشود. این متد برای زمانی مناسب است که به عنوان مثال شما از یک HorozinalScrollView استفاده کرده باشید. در این حالت layout شما هیچ گاه به پایان نمیرسد و بهتر هست عملیات اضافه را لغو کنید.
نگاهی به سورس
کلاس زیر شامل بخشهای زیر است:
فیلدهای خصوصی
//=-=--=-=-=-=-=-=-=-=-=-=-=- Private Properties -=-=-=-=-=-=-=--=-=-= private List<AndBreadCrumbItem> items=null; private List<TextView> textViews; private int tinyNextNodeImage; private int viewStyleId; private Context context; private boolean RTL; private float textSize=20; private boolean noResize=false; LinearLayout layout; IClickListener clickListener; ITextViewUpdate textViewUpdate; LinearLayout.LayoutParams params ;
اینترفیسها هم با حرف I شروع و برای تعریف رویدادها ایجاد شدهاند. در ادامه از تعدادی متد get و Set برای مقدار دهی بعضی از فیلدهای خصوصی بالا استفاده شده است:
//=-=---=-=-=-=-- Constructor =--=-=-=-=-=--=-=- public AndBreadCrumb(Context context) { this.context=context; params = new LinearLayout.LayoutParams (LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); } //=-=-=--=--=-=-=-=-=-=-=-=- Public Properties --=-=-=-=-=-=--=-=-=-=-=-=- //each category would be added to create path public void AddNewItem(AndBreadCrumbItem item) { if(items==null) items=new ArrayList<>(); items.add(item); } // if you want a pointer or next node between categories or textviews public void SetTinyNextNodeImage(int resId) {this.tinyNextNodeImage=resId;} public void SetViewStyleId(int resId) {this.viewStyleId=resId;} public void setTextSize(float textSize) {this.textSize = textSize;} public boolean isRTL() { return RTL; } public void setRTL(boolean RTL) { this.RTL = RTL; } public void setLayout(LinearLayout layout) { this.layout = layout; } public void setContext(Context context) { this.context = context; } public boolean isNoResize() { return noResize; } public void setNoResize(boolean noResize) { this.noResize = noResize; }
بعد از آن به متدهای خصوصی میرسیم که متد زیر، متد اصلی ما برای ساخت breadcrumb است:
//primary method for render objects on layout private void DrawPath() { //stop here if essentail elements aren't present if (items == null) return ; if (layout == null) return; if (items.size() == 0) return; //we need to get size of layout,so we use the post method to run this thread when ui is ready layout.post(new Runnable() { @Override public void run() { //textviews created here one by one int position = 0; textViews = new ArrayList<>(); for (AndBreadCrumbItem item : items) { TextView tv = MakeTextView(position, item.getId()); tv.setText(item.getDiplayText()); textViews.add(tv); position++; } //add textviews on layout AddTextViewsOnLayout(); //we dont manage resizing anymore if(isNoResize()) return; //run this code after textviews Added to get widths of them TextView last_tv=textViews.get(textViews.size()-1); last_tv.post(new Runnable() { @Override public void run() { //define width of each textview depend on screen width BatchSizeOperation(); } }); } }); }
TextView tv=new TextView(this); tv.getWidth(); //return 0 layout.add(tv); tv.getWidth(); //return 0
TextView tv=new TextView(this); tv.post(new Runnable() { @Override public void run() { tv.getWidth(); //return x } });
باز میگردیم به متد DrawPath و داخل متد post
در اولین خط این پروسه به ازای هر آیتم، یک TextView توسط متد MakeTextView ساخته میشود که شامل کد زیر است:
private TextView MakeTextView(final int position, final int Id) { //settings for cumbs TextView tv=new TextView(this.context); tv.setEllipsize(TextUtils.TruncateAt.END); tv.setSingleLine(true); tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); tv.setBackgroundResource(viewStyleId); /*call custom event - this event will be fired when user click on one of textviews and returns position of textview and value that user sat as id */ tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SetPosition(position); clickListener.onClick(position, Id); } }); //if user wants to update each textviews if(textViewUpdate!=null) tv=textViewUpdate.UpdateTextView(context,tv); if(isRTL()) tv.setRotationY(180); return tv; }
در خطوط اولیه، یک Textview ساخته و متد Ellipsize را با Truncate.END مقداردهی مینماید. این مقدار دهی باعث میشود اگر متن، در Textview جا نشد، ادامهی آن با ... مشخص شود. در خط بعدی Textview را تک خطه معرفی میکنیم. در خط بعدی اندازهی قلم را بر اساس آنچه کاربر مشخص کرده است، تغییر میدهیم و بعد هم استایل را برای آن مقداردهی میکنیم. بعد از آن رویداد کلیک را برای آن مشخص میکنیم تا اگر کاربر بر روی آن کلیک کرد، رویداد اختصاصی خودمان را فراخوانی کنیم.
در خط بعدی اگر rtl با true مقدار دهی شده باشد، textview را حول محور Y چرخش میدهد تا برای زبانهای راست به چپ چون فارسی آماده گردد و در نهایت Textview ساخته شده و به سمت متد DrawPath باز میگرداند.
بعد از ساخته شدن TextViewها، وقت آن است که به Layout اضافه شوند که وظیفهی اینکار بر عهدهی متد AddTextViewOnLayout است:
//this method calling by everywhere to needs add textviews on the layout like master method :drawpath private void AddTextViewsOnLayout() { //prepare layout //remove everything on layout for recreate it layout.removeAllViews(); layout.setOrientation(LinearLayout.HORIZONTAL); layout.setVerticalGravity(Gravity.CENTER_VERTICAL); if(isRTL()) layout.setRotationY(180); //add textviews one by one int position=0; for (TextView tv:textViews) { layout.addView(tv,params); //add next node image between textviews if user defined a next node image if(tinyNextNodeImage>0) if(position<(textViews.size()-1)) { layout.addView(GetNodeImage(), params); position++; } } }
تا به اینجا کار چیدمان به ترتیب انجام شده است ولی از آنجا که اندازهی Layout در هر گوشی و در دو حالت حالت افقی یا عمودی نگه داشتن گوشی متفاوت است، نمیتوان به این چینش اعتماد کرد که به چه نحوی عناصر نمایش داده خواهند شد و این مشکل توسط متد BatchSizeOperation (تغییر اندازه دسته جمعی) حل میگردد. در اینجا هم باز متد post به آخرین textview اضافه شده است. به این علت که موقعیکه همهی textviewها در ui جا خوش کردند، بتوانیم به خاصیتهای ui آنها دستیابی داشته باشیم. حالا بعد از ترسیم باید اندازه آنها را اصلاح کنیم. قدم به قدم متد BatchSizeOperation را بررسی میکنیم:
//set textview width depend on screen width private void BatchSizeOperation() { //get width of next node between cumbs Bitmap tinyBmap = BitmapFactory.decodeResource(context.getResources(), tinyNextNodeImage); int tinysize=tinyBmap.getWidth(); //get sum of nodes tinysize*=(textViews.size()-1); ... }
//get width size of screen(layout is screen here) int screenWidth=GetLayoutWidthSize(); //get sum of arrows and cumbs width int sumtvs=tinysize; for (TextView tv : textViews) { int width=tv.getWidth(); sumtvs += width; }
private int GetLayoutWidthSize() { int width=layout.getWidth(); int padding=layout.getPaddingLeft()+layout.getPaddingRight(); width-=padding; return width; }
private void BatchSizeOperation() { .... //if sum of cumbs is less than screen size the state is good so return same old textviews if(sumtvs<screenWidth) return ; if(textViews.size()>3) { //make fake path MakeFakePath(); //clear layout and add textviews again AddTextViewsOnLayout(); } //get free space without next nodes -> and spilt rest of space to textviews count to get space for each textview int freespace =screenWidth-tinysize; int each_width=freespace/textViews.size(); //some elements have less than each_width,so we should leave size them and calculate more space again int view_count=0; for (TextView tv:textViews) { if (tv.getWidth()<=each_width) freespace=freespace-tv.getWidth(); else view_count++; } if (view_count==0) return; each_width=freespace/view_count; for (TextView tv:textViews) { if (tv.getWidth()>each_width) tv.setWidth(each_width); } }
اگر آیتمها بیشتر از سه عدد باشند، میتوانیم از حالت مسیر جعلی استفاده کنیم که توسط متد MakeFakePath انجام میشود. البته بعد از آن هم باید دوباره viewها را چینش کنیم تا مسیر جدید ترسیم گردد، چون ممکن است بعد از آن باز هم جا نباشد یا آیتمها بیشتر از سه عدد نیستند. در این حالت، حداقل کاری که میتوانیم انجام دهیم این است که فضای موجود را بین آنها تقسیم کنیم تا همهی کاسه، کوزهها سر آیتم آخر نشکند و متنش به ... تغییر یابد و حداقل از هر آیتم، مقداری از متن اصلی نمایش داده شود. پس میانگین فضای موجود را گرفته و بر تعداد المانها تقسیم میکنیم. البته این را هم باید در نظر گرفت که در تقسیم بندی، بعضی آیتمها آن مقدار پهنا را نیاز ندارند و با پهنای کمتر هم میشود کل متنشان را نشان داد. پس یک کار اضافهتر این است که مقدار پهنای اضافی آنها را هم حساب کنیم و فقط آیتمهایی را پهنا دهیم که به مقدار بیشتری از این میانگین احتیاج دارند. در اینجا کار به پایان میرسد و مسیر نمایش داده میشود.
نحوهی کارکرد متد MakeFakePath بدین صورت است که 4 عدد TextView را ایجاد کرده که المانهای با اندیس 0 و 2 و 3 به صورت نرمال و عادی ایجاد شده و همان کارکرد سابق را دارند. ولی المان شماره دو با اندیس 1 با متن ... نمایندهی آیتمهای میانی است و رویدادکلیک آن به شکل زیر تحریف یافته است:
//if elements are so much(mor than 3),we make a fake path to decrease elements private void MakeFakePath() { //we make 4 new elements that index 1 is fake element and has a rest of real path in its heart //when user click on it,path would be opened textViews=new ArrayList<>(4); TextView[] tvs=new TextView[4]; int[] positions= {0,items.size()-3,items.size()-2,items.size()-1}; for (int i=0;i<4;i++) { //request for new textviews tvs[i]=MakeTextView(positions[i],items.get(positions[i]).getId()); if(i!=1) tvs[i].setText(items.get(positions[i]).getDiplayText()); else { tvs[i].setText("..."); //override click event and change it to part of code to open real path by call setposition method and redraw path tvs[i].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = items.size() - 3; int id = items.get(pos).getId(); SetPosition(items.size() - 3); clickListener.onClick(pos, id); } }); } textViews.add(tvs[i]); } }