ASP.NET MVC #22
تهیه سایتهای چند زبانه و بومی سازی نمایش اطلاعات در ASP.NET MVC
زمانیکه دات نت فریم ورک نیاز به انجام اعمال حساس به مسایل بومی را داشته باشد، ابتدا به مقادیر تنظیم شده دو خاصیت زیر دقت میکند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر این اساس دات نت میتواند تشخیص دهد که برای مثال خروجی متد DateTime.Now.ToString در کانادا و آمریکا باید با هم تفاوت داشته باشند. مثلا در آمریکا ابتدا ماه، سپس روز و در آخر سال نمایش داده میشود و در کانادا ابتدا سال، بعد ماه و در آخر روز نمایش داده خواهد شد. یا نمونهی دیگری از این دست میتواند نحوه نمایش علامت واحد پولی کشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روی بارگذاری فایلهای مخصوصی به نام Resource، تاثیر گذار است.
این خواص را یا به صورت دستی میتوان تنظیم کرد و یا ASP.NET، این اطلاعات را از هدر Accept-Language دریافتی از مرورگر کاربر به صورت خودکار مقدار دهی میکند. البته برای این منظور نیاز است یک سطر زیر را به فایل وب کانفیگ برنامه اضافه کرد:
<system.web>
<globalization culture="auto" uiCulture="auto" />
یا اگر نیاز باشد تا برنامه را ملزم به نمایش اطلاعات Resource مرتبط با فرهنگ بومی خاصی کرد نیز میتوان در همین قسمت مقادیر culture و uiCulture را دستی تنظیم نمود و یا اگر همانند برنامههایی که چند لینک را بالای صفحه نمایش میدهند که برای مثال به نگارشهای فارسی/عربی/انگلیسی اشاره میکند، اینکار را با کد نویسی نیز میتوان انجام داد:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");
جهت آزمایش این مطلب، ابتدا تنظیم globalization فوق را به فایل وب کانفیگ برنامه اضافه کنید. سپس به مسیر زیر در IE مراجعه کنید:
IE -> Tools -> Intenet options -> Genarl tab -> Languages
در اینجا میتوان هدر Accept-Language را مقدار دهی کرد. برای نمونه اگر مقدار زبان پیش فرض را به فرانسه تنظیم کنیم (به عنوان اولین زبان تعریف شده در لیست) و سپس سعی در نمایش مقدار decimal زیر را داشته باشیم:
string.Format("{0:C}", 10.5M)
اگر زبان پیش فرض، انگلیسی آمریکایی باشد، $ نمایش داده خواهد شد و اگر زبان به فرانسه تنظیم شود، یورو در کنار عدد مبلغ نمایش داده میشود.
تا اینجا تنها با تنظیم culture=auto به این نتیجه رسیدهایم. اما سایر قسمتهای صفحه چطور؟ برای مثال برچسبهای نمایش داده شده را چگونه میتوان به صورت خودکار بر اساس Accept-Language مرجح کاربر تنظیم کرد؟ خوشبختانه در دات نت، زیر ساخت مدیریت برنامههای چند زبانه به صورت توکار وجود دارد که در ادامه به بررسی آن خواهیم پرداخت.
آشنایی با ساختار فایلهای Resource
فایلهای Resource یا منبع، در حقیقت فایلهایی هستند مبتنی بر XML با پسوند resx و هدف آنها ذخیره سازی رشتههای متناظر با فرهنگهای مختلف میباشد و برای استفاده از آنها حداقل یک فایل منبع پیش فرض باید تعریف شود. برای نمونه فایل mydata.resx را در نظر بگیرید. برای ایجاد فایل منبع اسپانیایی متناظر، باید فایلی را به نام mydata.es.resx تولید کرد. البته نوع فرهنگ مورد استفاده را کاملتر نیز میتوان ذکر کرد برای مثال mydata.es-mex.resx جهت فرهنگ اسپانیایی مکزیکی بکارگرفته خواهد شد، یا mydata.fr-ca.resx به فرانسوی کانادایی اشاره میکند. سپس مدیریت منابع دات نت فریم ورک بر اساس مقدار CurrentUICulture جاری، اطلاعات فایل متناظری را بارگذاری خواهد کرد. اگر فایل متناظری وجود نداشت، از اطلاعات همان فایل پیش فرض استفاده میگردد.
حین تهیه برنامهها نیازی نیست تا مستقیما با فایلهای XML منابع کار کرد. زمانیکه اولین فایل منبع تولید میشود، به همراه آن یک فایل cs یا vb نیز ایجاد خواهد شد که امکان دسترسی به کلیدهای تعریف شده در فایلهای XML را به صورت strongly typed میسر میکند. این فایلهای خودکار، تنها برای فایل پیش فرض mydata.resx تولید میشوند،از این جهت که تعاریف اطلاعات سایر فرهنگهای متناظر نیز باید با همان کلیدهای فایل پیش فرض آغاز شوند. تنها «مقادیر» کلیدهای تعریف شده در کلاسهای منبع متفاوت هستند.
اگر به خواص فایلهای resx در VS.NET دقت کنیم، نوع Build action آنها به embedded resource تنظیم شده است.
مثالی جهت بررسی استفاده از فایلهای Resource
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. فایل وب کانفیگ آنرا ویرایش کرده و تنظیمات globalization ابتدای بحث را به آن اضافه کنید. سپس مدل، کنترلر و View متناظر با متد Index آنرا با محتوای زیر به پروژه اضافه نمائید:
namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
using System.Web.Mvc;
using MvcApplication19.Models;
namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}
@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>
قصد داریم در View فوق بر اساس uiCulture کاربر مراجعه کننده به سایت، برچسب Name را مقدار دهی کنیم. اگر کاربری از ایران مراجعه کند، «نام کارمند» نمایش داده شود و سایر کاربران، «Employee Name» را مشاهده کنند. همچنین این تغییرات باید بر روی متد Html.DisplayForModel نیز تاثیرگذار باشد.
برای این منظور بر روی پوشه Views/Home که محل قرارگیری فایل Index.cshtml فوق است کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس در صفحه ظاهر شده، گزینه «Resources file» را انتخاب کرده و برای مثال نام Index_cshtml.resx را وارد کنید.
به این ترتیب اولین فایل منبع مرتبط با View جاری که فایل پیش فرض نیز میباشد ایجاد خواهد شد. این فایل، به همراه فایل Index_cshtml.Designer.cs تولید میشود. سپس همین مراحل را طی کنید، اما اینبار نام Index_cshtml.fa.resx را حین افزودن فایل منبع وارد نمائید که برای تعریف اطلاعات بومی ایران مورد استفاده قرار خواهد گرفت. فایل دومی که اضافه شده است، فاقد فایل cs همراه میباشد.
اکنون فایل Index_cshtml.resx را در VS.NET باز کنید. از بالای صفحه، به کمک گزینه Access modifier، سطح دسترسی متدهای فایل cs همراه آنرا به public تغییر دهید. پیش فرض آن internal است که برای کار ما مفید نیست. از این جهت که امکان دسترسی به متدهای استاتیک تعریف شده در فایل خودکار Index_cshtml.Designer.cs را در View های برنامه، نخواهیم داشت. سپس دو جفت «نام-مقدار» را در فایل resx وارد کنید. مثلا نام را Name و مقدار آنرا «Employee Name» و سپس نام دیگر را NameIsNotRight و مقدار آنرا «Name is required» وارد نمائید.
در ادامه فایل Index_cshtml.fa.resx را باز کنید. در اینجا نیز دو جفت «نام-مقدار» متناظر با فایل پیش فرض منبع را باید وارد کرد. کلیدها یا نامها یکی است اما قسمت مقدار اینبار باید فارسی وارد شود. مثلا نام را Name و مقدار آنرا «نام کارمند» وارد نمائید. سپس کلید یا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائید» را تنظیم نمائید.
تا اینجا کار تهیه فایلهای منبع متناظر با View جاری به پایان میرسد.
در ادامه با کمک فایل Index_cshtml.Designer.cs که هربار پس از تغییر فایل resx متناظر آن به صورت خودکار توسط VS.NET تولید و به روز میشود، میتوان به کلیدها یا نامهایی که تعریف کردهایم، در قسمتهای مختلف برنامه دست یافت. برای نمونه تعریف کلید Name در این فایل به نحو زیر است:
namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}
بنابراین برای استفاده از آن در هر View ایی تنها کافی است بنویسیم:
@MvcApplication19.Views.Home.Index_cshtml.Name
به این ترتیب بر اساس تنظیمات محلی کاربر، اطلاعات به صورت خودکار از فایلهای Index_cshtml.fa.resx فارسی یا فایل پیش فرض Index_cshtml.resx، دریافت میگردد.
علاوه بر امکان دسترسی مستقیم به کلیدهای تعریف شده در فایلهای منبع، امکان استفاده از آنها توسط data annotations نیز میسر است. در این حالت میتوان مثلا پیغامهای اعتبار سنجی را بومی کرد یا حین استفاده از متد Html.DisplayForModel، بر روی برچسب نمایش داده شده خودکار، تاثیر گذار بود. برای اینکار باید اندکی مدل برنامه را ویرایش کرد:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}
همانطور که ملاحظه میکنید، حین تعریف ویژگیهای Display یا Required، امکان تعریف نام کلاس متناظر با فایل resx خاصی وجود دارد. به علاوه ErrorMessageResourceName به نام یک کلید در این فایل و یا پارامتر Name ویژگی Display نیز به نام کلیدی در فایل منبع مشخص شده، اشاره میکنند. این اطلاعات توسط متدهای Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودکار مورد استفاده قرار خواهند گرفت.
نکتهای در مورد کش کردن اطلاعات
در این مثال اگر فیلتر OutputCache را بر روی متد Index تعریف کنیم، حتما نیاز است به هدر Accept-Language نیز دقت داشت. در غیراینصورت تمام کاربران، صرفنظر از تنظیمات بومی آنها، یک صفحه را مشاهده خواهند کرد:
[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()
مطابق RFC مربوطه، اگر هدر درخواست ارسالی به سرور را کمی تغییر دهیم میتوان بجای شروع از اولین بایت، از بایت مورد نظر شروع به دریافت فایل نمود. (البته این به شرطی است که سرور آنرا پشتیبانی کند)
یعنی نیاز داریم که به هدر ارسالی سطر زیر را اضافه کنیم:
Range: bytes=n-
برای بدست آوردن اندازهی فایل ناقص موجود میتوان از دستور زیر استفاده کرد:
using System.IO;
long brokenLen = new FileInfo(fileNamePath).Length;
سپس اگر شیء webRequest ما به صورت زیر تعریف شده باشد:
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
فقط کافی است سطر زیر را جهت افزودن قابلیت از سرگیری مجدد دریافت فایل به این شیء افزود:
//دانلود از ادامه
webRequest.AddRange((int)brokenLen); //resume
نکته:
اگر علاقمند باشید که ریز فعالیتهای انجام شده توسط فضای نام System.Net را ملاحظه کنید، به فایل config خود (مثلا فایل app.config برنامه)، چند سطر زیر را اضافه کنید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.Net">
<listeners>
<add name="MyTraceFile"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add
name="MyTraceFile"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="System.Net.trace.log"
/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose" />
</switches>
</system.diagnostics>
</configuration>
ملاحظات:
بدیهی است پیاده سازی قابلیت resume نیاز به موارد زیر خواهد داشت:
الف) در نظر گرفتن مسیری پیش فرض برای ذخیره سازی فایلها
ب) پیدا کردن اندازهی فایل موجود بر روی یک سرور و مقایسهی آن با حجم فایل موجود بر روی هارد
امکان پیدا کردن اندازهی یک فایل هم بدون دریافت کامل آن میسر است. خاصیت ContentLength مربوط به شیء HttpWebResponse بیانگر اندازهی یک فایل بر روی سرور است و صد البته پیش از استفاده از این عدد، مقدار StatusCode شیء نامبرده را بررسی کنید. اگر مساوی OK بود، یعنی این عدد معتبر است.
در این قسمت ابتدا نحوهی فعال سازی فریم ورک آزمونهای واحد مایکروسافت و سپس نحوهی فعال سازی این تامین کنندهی بانک اطلاعاتی درون حافظهای را بررسی خواهیم کرد. به علاوه برای سرویس بلاگهای قسمت قبل نیز آزمون واحد خواهیم نوشت.
نحوهی فعالسازی فریم ورک MSTest در یک پروژهی Class library از نوع NET Core.
تنها نکتهی مهم فعالسازی MSTest در یک پروژهی Class library جدید که برای نوشتن آزمونهای واحد مورد استفاده قرار خواهیم داد، تنظیمات فایل project.json آن است که در ذیل آمده است:
{ "version": "1.0.0-*", "testRunner": "mstest", "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "dotnet-test-mstest": "1.1.1-preview", "MSTest.TestFramework": "1.0.1-preview", "NETStandard.Library": "1.6.0", "Microsoft.EntityFrameworkCore": "1.0.0", "Microsoft.EntityFrameworkCore.InMemory": "1.0.0", "Core1RtmEmptyTest.DataLayer": "1.0.0-*", "Core1RtmEmptyTest.Entities": "1.0.0-*", "Core1RtmEmptyTest.Services": "1.0.0-*", "Core1RtmEmptyTest.ViewModels": "1.0.0-*" }, "frameworks": { "netcoreapp1.0": { "imports": [ "dnxcore50", "portable-net45+win8" ] } } }
- به علاوه در اینجا ارجاعاتی را به اسمبلیهای موجودیتها، Services و DataLayer که در قسمت «شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگیها» بررسی شدند نیز ملاحظه میکنید.
- همچنین وابستگی جدید Microsoft.EntityFrameworkCore.InMemory نیز در اینجا قابل ملاحظه است. این وابستگی را تنها به پروژهی آزمونهای واحد خود اضافه میکنیم. از این جهت که تنظیمات آن صرفا در این قسمت جدید قید میشوند و نه در سایر قسمتهای برنامه.
پس از آن، کار با این فریم ورک، همانند سایر نگارشهای دات نت خواهد بود:
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFCore.MsTests { [TestClass] public class CoreTests { [TestMethod] public void Test1() { Assert.IsTrue(true); } } }
پس از نوشتن اولین آزمون واحد، یکبار پروژه را build کرده و سپس از منوی Test، گزینهی Windows را انتخاب کرده و در اینجا گزینهی Test Explorer را انتخاب کنید. اندکی صبر کنید تا آزمونهای واحد شما شناسایی شوند و سپس گزینهی Run All را انتخاب کنید:
تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای
در مورد نحوهی تعریف و افزودن وابستگیهای EF Core در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» پیشتر بحث شد و همچنین در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرتها به یک اسمبلی دیگر»، اطلاعات Context برنامه را به اسمبلی دیگری منتقل کردیم.
اگر از روش بازنویسی متد OnConfiguring برای تنظیم تامین کنندهی بانک اطلاعاتی مورد نظر استفاده میکنید، متد OnConfiguring کلاس Context برنامه چنین شکلی را پیدا میکند:
public class ApplicationDbContext : DbContext, IUnitOfWork { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer( _configuration["ConnectionStrings:ApplicationDbContextConnection"] , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); }); } }
الف) اضافه شدن سازندهی دومی که <DbContextOptions<ApplicationDbContext را دریافت میکند. از آن در سمت کدهای آزمون واحد برنامه جهت ثبت ()options.UseInMemoryDatabase استفاده میشود.
ب) به متد OnConfiguring، بررسی optionsBuilder.IsConfigured هم اضافه شدهاست. چون در سمت کدهای آزمون واحد، تامین کنندهی بانک اطلاعاتی درون حافظهای اضافه میشود، مقدار optionsBuilder.IsConfigured به true تنظیم خواهد شد و دیگر از تامین کنندهی SQL Server استفاده نمیشود.
اگر از متد OnConfiguring به این شکل استفاده نمیکنید، تنها ذکر سازندهی دوم ضروری است. از این جهت که در آزمونهای واحد، از تنظیمات متد ConfigureServices کلاس آغازین برنامه استفاده نخواهد شد.
نوشتن آزمونهای واحد مخصوص EF Core
پس از برپایی پیشنیازهای نوشتن آزمونها واحد، شامل تنظیمات فریم ورک MSTest و همچنین افزودن وابستگیهای مرتبط با فایل project.json ایی که در ابتدای بحث عنوان شد و اصلاح سازنده و متد OnConfiguring کلاس Context برنامه جهت آماده سازی آنها برای پذیرش تامین کنندههای دیگر، اکنون یک نمونه از آزمونهای واحد درون حافظهای EF Core، چنین شکلی را خواهد داشت:
using System; using System.Linq; using Core1RtmEmptyTest.DataLayer; using Core1RtmEmptyTest.Entities; using Core1RtmEmptyTest.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Core1RtmEmptyTest.MsTests { [TestClass] public class CoreTests { private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); } [TestMethod] public void Find_searches_url() { // Insert seed data into the database using one instance of the context using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { context.Set<Blog>().Add(new Blog { Url = "http://sample.com/cats" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/catfish" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/dogs" }); context.SaveAllChanges(); } } // Use a separate instance of the context to verify correct data was saved to database using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { Assert.AreEqual(3, context.Set<Blog>().Count()); Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url); } } // Use a clean instance of the context to run the test using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var blogService = serviceScope.ServiceProvider.GetRequiredService<IBlogService>(); var results = blogService.GetPagedBlogsAsNoTracking(pageNumber: 0, recordsPerPage: 10); Assert.AreEqual(3, results.Count); } } } }
همانطور که در قسمت «تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای» فوق عنوان شد، در حین انجام آزمونهای واحد، دیگر به کلاس آغازین برنامه و تنظیمات آن مراجعه نمیشود. بنابراین باید شبیه به عملکرد متد ConfigureServices آنرا در اینجا پیاده سازی کرد. نمونهای از انجام اینکار را در سازندهی کلاس انجام آزمونهای واحد مشاهده میکنید:
private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); }
سپس همانند قبل، باید تمام سرویسهای مدنظر تنظیم شوند تا بتوان از آنها استفاده کرد.
نکتهی مهم دیگری را که باید به آن دقت داشت، ایجاد scope و سپس دسترسی به سرویسها از طریق این Scope است. از این جهت که چون خارج از طول عمر یک درخواست وب قرار داریم، دیگر Scopeها برای ما به صورت خودکار ایجاد و تخریب نمیشوند و باید همانکاری را که ASP.NET Core در پشت صحنه انجام میدهد، به صورت دستی پیاده سازی کنیم:
using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) {
یک نکتهی تکمیلی
EF Core به همراه تامین کنندهی بانک اطلاعاتی SQLite نیز هست. یکی از نکات ویژهی بانک اطلاعاتی SQLite، امکان تنظیم پارامتری است در رشتهی اتصالی آن، که آنرا نیز تبدیل به یک «بانک اطلاعاتی درون حافظهای» میکند. این روش سالها است که جهت انجام آزمونهای واحد ORMها مورد استفاده قرار میگیرد. بنابراین میتوان آنرا به عنوان جایگزینی برای مطلب جاری نیز درنظر گرفت.
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); services.AddEntityFrameworkSqlite().AddDbContext<CmsDbContext>(options => options.UseSqlite(connection));
عموما برای درج فایلهای ثابت اسکریپتها و شیوهنامههای سایت، از روش متداول زیر استفاده میشود:
<link rel="stylesheet" href="/css/site.css" /> <script src="/js/site.js"></script>
مشکلی که به همراه این روش وجود دارد، مطلع سازی کاربران و مرورگر، از تغییرات آنهاست؛ چون این فایلهای ثابت، توسط مرورگرها کش شده و با فشردن دکمههایی مانند Ctrl+F5 و بهروز شدن کش مرورگر، به نگارش جدید، ارتقاء پیدا میکنند. برای رفع این مشکل حداقل دو روش وجود دارد:
الف) هربار نام این فایلها را تغییر دهیم. برای مثال بجای نام قدیمی site.css، از نام جدید site.v.1.1.css استفاده کنیم.
ب) یک کوئری استرینگ متغیر را به نام ثابت این فایلها، اضافه کنیم.
که در این بین، روش دوم متداولتر و معقولتر است. برای این منظور، ASP.NET Core به همراه ویژگی توکاری است به نام asp-append-version که اگر آنرا به تگهای اسکریپت و link اضافه کنیم:
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <script src="~/js/site.js" asp-append-version="true"></script>
این کوئری استرینگ را به صورت خودکار محاسبه کرده و به آدرس فایل درج شده اضافه میکند؛ با خروجیهایی شبیه به مثال زیر:
<link rel="stylesheet" href="/css/site.css?v=AAs5qCYR2ja7e8QIduN1jQ8eMcls-cPxNYUozN3TJE0" /> <script src="/js/site.js?v=NO2z9yI9csNxHrDHIeTBBfyARw3PX_xnFa0bz3RgnE4"></script>
ASP.NET Core در اینجا هش فایلهای یافت شده را با استفاده از الگوریتم SHA256 محاسبه و url encode کرده و به صورت یک کوئری استرینگ، به انتهای آدرس فایلها اضافه میکند. به این ترتیب با تغییر محتوای این فایلها، این هش نیز تغییر میکند و مرورگر بر این اساس، همواره آخرین نگارش ارائه شده را از سرور دریافت خواهد کرد. نتیجهی این محاسبات نیز به صورت خودکار کش میشود و همچنین با استفاده از یک File Watcher در پشت صحنه، تغییرات این فایلها هم بررسی میشوند. یعنی اگر فایلی تغییر کرد، نیازی به ریاستارت برنامه نیست و محاسبات جدید و کش شدن مجدد آنها، به صورت خودکار انجام میشود.
البته این ویژگی هنوز به Blazor اضافه نشدهاست؛ اما امکان استفادهی از زیر ساخت ویژگی asp-append-version با کدنویسی مهیا است که در ادامه با استفاده از آن، کامپوننتی را مخصوص Blazor SSR، تهیه میکنیم.
دسترسی به زیر ساخت محاسباتی ویژگی asp-append-version با کدنویسی
زیرساخت محاسباتی ویژگی asp-append-version، با استفاده از سرویس توکار IFileVersionProvider به صورت زیر قابل دسترسی است:
public static class FileVersionHashProvider { private static readonly string ProcessExecutableModuleVersionId = Assembly.GetEntryAssembly()!.ManifestModule.ModuleVersionId.ToString("N"); public static string GetFileVersionedPath(this HttpContext httpContext, string filePath, string? defaultHash = null) { ArgumentNullException.ThrowIfNull(httpContext); var fileVersionedPath = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>() .AddFileVersionToPath(httpContext.Request.PathBase, filePath); return IsEmbeddedOrNotFound(fileVersionedPath, filePath) ? QueryHelpers.AddQueryString(filePath, new Dictionary<string, string?>(StringComparer.Ordinal) { { "v", defaultHash ?? ProcessExecutableModuleVersionId } }) : fileVersionedPath; } private static bool IsEmbeddedOrNotFound(string fileVersionedPath, string filePath) => string.Equals(fileVersionedPath, filePath, StringComparison.Ordinal); }
در برنامههای Blazor SSR، دسترسی کاملی به HttpContext وجود دارد و همانطور که مشاهده میکنید، این سرویس نیز به اطلاعات آن جهت محاسبهی هش فایل معرفی شدهی به آن، نیاز دارد. در اینجا اگر هش قابل محاسبه نبود، از هش فایل اسمبلی جاری استفاده خواهد شد.
ساخت کامپوننتهایی برای درج خودکار هش فایلهای اسکریپتها
یک نمونه روش استفادهی از متد الحاقی GetFileVersionedPath فوق را در کامپوننت DntFileVersionedJavaScriptSource.razor زیر میتوانید مشاهده کنید:
@if (!string.IsNullOrWhiteSpace(JsFilePath)) { <script src="@HttpContext.GetFileVersionedPath(JsFilePath)" type="text/javascript"></script> } @code{ [CascadingParameter] public HttpContext HttpContext { set; get; } = null!; [Parameter] [EditorRequired] public required string JsFilePath { set; get; } }
با استفاده از HttpContext مهیای در برنامههای Blazor SSR، متد الحاقی GetFileVersionedPath به همراه مسیر فایل js. مدنظر، در صفحه درج میشود.
برای مثال یک نمونه از استفادهی آن، به صورت زیر است:
<DntFileVersionedJavaScriptSource JsFilePath="/lib/quill/dist/quill.js"/>
در نهایت با اینکار، یک چنین خروجی در صفحه درج خواهد شد که با تغییر محتوای فایل quill.js، هش متناظر با آن به صورت خودکار بهروز خواهد شد:
<scriptsrc="/lib/quill/dist/quill.js?v=5q7uUOOlr88Io5YhQk3lgYcoB_P3-5Awq1lf0rRa7-Y" type="text/javascript"></script>
شبیه به همین کار را برای شیوهنامهها هم میتوان تکرار کرد و کدهای آن، تفاوت آنچنانی با کامپوننت فوق ندارند.
private Document oDoc; public void createdoc1() { var realpath="~/template"; var filePath = Path.Combine(HttpContext.Current.Server.MapPath("~/template"), Lcourseid.Text + ".doc"); var oWordApplication = new Application(); DirectoryInfo dir = new DirectoryInfo(Server.MapPath(realpath)); foreach (FileInfo files in dir.GetFiles()) { files.Delete(); } // To invoke MyMethod with the default argument value, pass // Missing.Value for the optional parameter. object missing = System.Reflection.Missing.Value; //object fileName = ConfigurationManager.AppSettings["DocxPath"];@"C:\DocXExample.docx"; string fileName = @"D:\template1.dot"; //string fileName1 = @"D:\sss.doc"; object newTemplate = false; object docType = 0; object isVisible = true; //System.Reflection.Missing.Value is used here for telling that method to use default parameter values when method execution oDoc = oWordApplication.Documents.Open(fileName, newTemplate, docType, isVisible, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing); // usable in earlier versions of Microsoft Word v2003 v11 // if(Convert.ToInt16(oWordApplication.Version) >=11) { //Sets or returns a Boolean that represents whether a document is being viewed in reading layout view. oDoc.ActiveWindow.View.ReadingLayout = false; } //The active window is the window that currently has focus.If there are no windows open, an exception is thrown. //microsoft.office.tools.word. oDoc.Activate(); if (oDoc.Bookmarks.Exists("Title")) { oDoc.Bookmarks["Title"].Range.Text = "Test Field Entry from webform"; oDoc.Bookmarks["Address"].Range.Text = "Address Field Entry from webform"; } oDoc.SaveAs(filePath, ref missing); oWordApplication.Documents.Close(ref missing, ref missing, ref missing); //oWordApplication.Quit(ref SaveChanges, ref missing, ref missing, ref missing); ProcessRequest(filePath, Lcourseid.Text);
همان طور که میدانید کاربرد پذیری در خیلی از
پروژهها حرف اول رو میزند و کاربر دوست دارد کارهایی که انجام میدهد خیلی راحت
و با استفاده از موس باشد.یکی از کار هایی که در اکثر پروژهها نیاز است ، چیدمان
ترتیب رکوردها است. ما میخواهیم در این پست ترتیبی اتخاذ کنیم که کاربر بتواند رکوردها را به هر ترتیبی که دوست دارد نمایش دهد.
از توضیحاتی که قبلا دادم مشخص است که این کار احتمالا در ASP.NET WebForm کار سختی نیست ولی این کار باید در MVC از ابتدا طراحی شود.
طرح سوال : یک سری رکورد از یک Table داریم که میخواهیم به ترتیب وارد شدن رکوردها نباشد و ترتیبی که ما میخواهیم نمایش داده شود.
پاسخ کوتاه : خب باید ابتدا یک فیلد (برای اولویت بندی) به Table اضافه کنیم بعد اون فیلد رو بنا به ترتیبی که دوست داریم رکوردها نمایش داده شود پر کنیم (Sort می کنیم ) و در آخر هم هنگام نمایش در View رکوردها را بر اساس این فیلد نمایش میدهیم.
(این پست هم در ادامه پست قبلی در همان پروژه است و از همان Table ها استفاده شده است)
اضافه کردن فیلد :
ابتدا یک فیلد به Table مورد نظر اضافه میکنیم. من اسم این فیلد رو Priority گذاشتم. Table من چنین وضعیتی دارد.
افزودن فایلهای jQuery UI :
در این مرحله شما نیاز دارید فایلهای مورد نیاز برای Sort کردن رکوردها را اضافه کنید. شما میتوانید فقط فایلهای مربوط به Sortable را به صفحه خودتان اضافه کنید و یا مثل من فایل هایی که حاوی تمام قسمتهای jQuery UI هست را اضافه کنید.
من برای این کار از Section استفاده کردم ، ابتدا در Head فایل Layout دو Section تعریف کردم برای CSS و JavaScript . و فایلهای مربوط به Sort کردن را در صفحه ای که باید عمل Sort انجام بشود در این Section ها قرار دادم.
فایل Layout
<head> <meta charset="utf-8" /> @RenderSection("meta", false) <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/~Site.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/redactor/css/redactor.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/css/bootstrap-rtl.min.css")" rel="stylesheet" type="text/css" /> @RenderSection("css", false) <script src="@Url.Content("~/Scripts/jquery-1.8.2.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/js/bootstrap-rtl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Content/redactor/redactor.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> @RenderSection("js", false) </head>
فایل Index.chtml در پوشه کنترلر Type
@model IEnumerable<KhazarCo.Models.Type> @{ ViewBag.Title = "Index"; Layout = "~/Areas/Administrator/Views/Shared/_Layout.cshtml"; } @section css {<link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" /> } @section js { <script src="@Url.Content("~/Scripts/jquery-ui-1.9.0.min.js")" type="text/javascript"></script> }
در آخر فایل Index.chtml به اینصورت شده است:
<h2> نوع ها</h2> <p> @Html.ActionLink("ایجاد یک مورد جدید", "Create", null, new { @class = "btn btn-info" }) </p> <table> <thead> <tr> <th> عنوان </th> <th> توضیحات </th> <th> فعال </th> <th> </th> </tr> </thead> <tbody> @foreach (var item in Model.OrderBy(m => m.Priority)) { <tr id="@item.Id"> <td> @Html.DisplayFor(modelItem => item.Title) </td> <td> @(new HtmlString(item.Description)) </td> <td> @Html.DisplayFor(modelItem => item.IsActive) </td> <td> @Html.ActionLink("ویرایش", "Edit", new { id = item.Id }, new { @class = "btnEdit label label-warning" }) | @Html.ActionLink("مشاهده", "Details", new { id = item.Id }, new { @class = "btnDetails label label-info" }) | @Html.ActionLink("حذف", "Delete", new { id = item.Id }, new { @class = "btnDelete label label-important" }) </td> </tr> } </tbody> </table>
** توجه داشته باشید که من به هر tr یک id اختصاص داده ام که این مقدار id همان مقدار فیلد Id همان رکورد هست ، ما برای مرتب کردن به این Id نیاز داریم (خط 25).
افزودن کدهای کلاینت:
حالا باید کدی بنویسم که دو کار را برای ما انجام دهد : اول حالت Sort پذیری را به سطرهای Table بدهد و دوم اینکه هنگامی که ترتیب سطرهای تغییر کرد ما را با خبر کند:
<script type="text/javascript"> $(function () { $("table tbody").sortable({ helper: fixHelper, update: function (event, ui) { jQuery.ajax('@Url.Action("Sort", "Type", new { area = "Administrator" })', { data: { s: $(this).sortable('toArray').toString() } }); } }).disableSelection(); }); var fixHelper = function (e, ui) { ui.children().each(function () { $(this).width($(this).width()); }); return ui; }; </script>
توضیح کد :
در این کد ما حالت ترتیب پذیری را به Table می دهیم و هنگامی که عمل Update در Table انجام شد تابع مربوطه اجرا میشود. ما در این تایع، ترتیب جدید سطرها را میگیریم ( ** به کمک مقدار Id که به هر سطر دادیم ، این مقدار Id برابر بود با Id خود رکورد در Database ) و به کمکjQuery.ajax به تابع Sort از کنترلر Type در منطقه (area ) Administrator ارسال میکنیم و در آنجا ادامه کار را انجام میدهیم.
تابع fixHelper هم به ما کمک میکند که هنگامی که سطرها از جای خود جدا میشوند ، دارای عرض یکسانی باشند و عرض آنها تغییری نکند.
افزودن کد Server:
حالا باید تابع Sort که مقادیر را به آن ارسال کردیم بنویسم. من این تابع را بر اساس مقداری که از کلاینت ارسال میشود اینگونه طراحی کردم.
public EmptyResult Sort(string s) { if (s != null) { var ids = new List<int>(); foreach (var item in s.Split(',')) { ids.Add(int.Parse(item)); } int intpriority = 0; foreach (var item in ids) { intpriority++; db.Types.Single(m => m.Id == item).Priority = intpriority; } db.SaveChanges(); } return new EmptyResult(); }
در ایتدا مقادیر Id که از کلاینت به صورت String ارسال شده است را میگیریم و بعد به همان ترتیب ارسال در لیستی از int قرار میدهیم ids.
سپس به اضای هر رکورد Type مقدار اولویت را به فیلدی که برای همین مورد اضافه کردیم Priority اختصاص میدهیم. و در آخر هم تغییرات را ذخیره میکنیم. (خود کد کاملا واضح است و نیاری به توضیح بیشتر نیست )
حالا باید هنگامی که لیست Type ها نمایش داده میشود به ترتیب (OrderBy) فیلد Priority نمایش داده شود پس تابع Index را اینطور تغییر میدهیم.
public ViewResult Index() { return View(db.Types.Where(m => m.IsDeleted == false).OrderBy(m => m.Priority)); }
این هم خروجی کار من:
این عکس مربوط به است به قسمت مدیریت پروژه شیرآلات مرجان خزر.
ثبت و نگهداری تاریخ خورشیدی در SQL Server از دیرباز یکی از نگرانیهای برنامهنویسان و
طراحان پایگاه دادهها بوده است. در این نوشتار، راهکار تعریف یک DataType در SQL Server 2012 به روش CLR آموزش داده
خواهد شد.
در ویژوال استودیو یک پروژهی جدید از نوع SQL Server Database Project
به شکل زیر ایجاد کنید:
متن موجود در صفحهی بازشده را کاملاً حذف کرده و با کد زیر جایگزین کنید.
(در کد زیر همهی توابع لازم برای مقداردهی به سال، ماه، روز، ساعت، دقیقه و ثانیه و البته گرفتن مقدار از آنها، تبدیل تاریخ خورشیدی به میلادی، گرفتن تاریخ به تنهایی، گرفتن زمان به تنهایی، افزایش یا کاهش زمان برپایهی یکی از متغیرهای زمان و بررسی و اعتبارسنجی انواع بخشهای زمان گنجانده شده است. در صورت پرسش یا پیشنهاد روی هر کدام در قسمت نظرات، پیام خود را بنویسید.)
using System; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; [Serializable()] [SqlUserDefinedType(Format.Native)] public struct JalaliDate : INullable { private Int16 m_Year; private byte m_Month; private byte m_Day; private byte m_Hour; private byte m_Minute; private byte m_Second; private bool is_Null; public Int16 Year { get { return (this.m_Year); } set { m_Year = value; } } public byte Month { get { return (this.m_Month); } set { m_Month = value; } } public byte Day { get { return (this.m_Day); } set { m_Day = value; } } public byte Hour { get { return (this.m_Hour); } set { m_Hour = value; } } public byte Minute { get { return (this.m_Minute); } set { m_Minute = value; } } public byte Second { get { return (this.m_Second); } set { m_Second = value; } } public bool IsNull { get { return is_Null; } } public static JalaliDate Null { get { JalaliDate jl = new JalaliDate(); jl.is_Null = true; return (jl); } } public override string ToString() { if (this.IsNull) { return "NULL"; } else { return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2"); } } public static JalaliDate Parse(SqlString s) { if (s.IsNull) { return Null; } System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar(); string str = Convert.ToString(s); string[] JDate = str.Split(' ')[0].Split('/'); JalaliDate jl = new JalaliDate(); jl.Year = Convert.ToInt16(JDate[0]); byte MonthsInYear = (byte)pers.GetMonthsInYear(jl.Year); jl.Month = (byte.Parse(JDate[1]) <= MonthsInYear ? (byte.Parse(JDate[1]) > 0 ? byte.Parse(JDate[1]) : (byte)1) : MonthsInYear); byte DaysInMonth = (byte)pers.GetDaysInMonth(jl.Year, jl.Month); ; jl.Day = (byte.Parse(JDate[2]) <= DaysInMonth ? (byte.Parse(JDate[2]) > 0 ? byte.Parse(JDate[2]) : (byte)1) : DaysInMonth); if (str.Split(' ').Length > 1) { string[] JTime = str.Split(' ')[1].Split(':'); jl.Hour = (JTime.Length >= 1 ? (byte.Parse(JTime[0]) < 23 && byte.Parse(JTime[0]) >= (byte)0 ? byte.Parse(JTime[0]) : (byte)0) : (byte)0); jl.Minute = (JTime.Length >= 2 ? (byte.Parse(JTime[1]) < 59 && byte.Parse(JTime[1]) >= (byte)0 ? byte.Parse(JTime[1]) : (byte)0) : (byte)0); jl.Second = (JTime.Length >= 3 ? (byte.Parse(JTime[2]) < 59 && byte.Parse(JTime[2]) >= (byte)0 ? byte.Parse(JTime[2]) : (byte)0) : (byte)0); } else { jl.Hour = 0; jl.Minute = 0; jl.Second = 0; } return (jl); } public SqlString GetDate() { return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2"); } public SqlString GetTime() { return this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2"); } public SqlDateTime ToGregorianTime() { System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar(); return SqlDateTime.Parse(pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0).ToString()); } public SqlString JalaliDateAdd(SqlString interval, int increment) { System.Globalization.PersianCalendar pers = new System.Globalization.PersianCalendar(); DateTime dt = pers.ToDateTime(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0); string CInterval = interval.ToString(); bool isConvert = true; switch (CInterval) { case "Year": dt = pers.AddYears(dt, increment); break; case "Month": dt = pers.AddMonths(dt, increment); break; case "Day": dt = pers.AddDays(dt, increment); break; case "Hour": dt = pers.AddHours(dt, increment); break; case "Minute": dt = pers.AddMinutes(dt, increment); break; case "Second": dt = pers.AddSeconds(dt, increment); break; default: isConvert = false; break; } if (isConvert == true) { this.Year = (Int16)pers.GetYear(dt); this.Month = (byte)pers.GetMonth(dt); this.Day = (byte)pers.GetDayOfMonth(dt); this.Hour = (byte)pers.GetHour(dt); this.Minute = (byte)pers.GetMinute(dt); this.Second = (byte)pers.GetSecond(dt); } return this.m_Year.ToString("D4") + "/" + this.m_Month.ToString("D2") + "/" + this.m_Day.ToString("D2") + " " + this.Hour.ToString("D2") + ":" + this.Minute.ToString("D2") + ":" + this.Second.ToString("D2"); } }
از منوهای بالا روی منوی Bulild و سپس گزینهی Publish prgJalaliDate کلیک کتید:
در پنجرهی بازشده روی دکمهی Edit کلیک کنید سپس تنظیمات مربوط به اتصال به پایگاه داده را انجام دهید.
روی دکمهی OK کلیک کنید و سپس در پنجرهی اولیه، روی دکمهی Publish کلیک کتید:
به همین سادگی، DataType مربوطه در SQL Server 2012 ساخته میشود. خبر خوش اینکه شما میتوانید با راستکلیک روی نام پروژه و انتخاب گزینهی Properties در قسمت Project Setting تنظیمات مربوط به نگارش SQL Server را انجام دهید. (از نگارش 2005 به بعد در VS 2012 پشتیبانی میشود.)
اکنون زمان آن رسیده است که DataType ایجادشده را در SQL Server 2012 بیازماییم. SQL Server را باز کنید و دستور زیر را در آن اجرا کتید.
USE Northwind GO CREATE TABLE dbo.TestTable ( Id int NOT NULL IDENTITY (1, 1), TestDate dbo.JalaliDate NULL ) ON [PRIMARY] GO
اکنون چند رکورد درون این جدول درج میکنیم:
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
این خطا به این خاطر است که CLR را در SQL Server فعال نکرده ایم. جهت فعالکردن CLR دستور زیر را اجرا کنید:
sp_configure 'clr enabled', 1 Reconfigure
Insert into TestTable (TestDate) Values ('1392/02/09'),('1392/02/09 22:40'),('1392/12/30 22:40')
اکنون زمان آن رسیده است که توسط یک پرسوجو، همهی توابعی که در سیشارپ برای این نوع داده نوشتیم، بیازماییم. پرسوجوی زیر را اجرا کنید:
Select TestDate.ToString() as JalaliDateTime, TestDate.GetDate() as JalaliDate, TestDate.GetTime() as JalaliTime, TestDate.ToGregorianTime() as GregorianTime, TestDate.JalaliDateAdd('Day',1) JalaliTomorrow, TestDate.Month as JalaliMonth from TestTable
نیازی به گفتن نیست که میتوانید به سادگی از توابع مربوط به DateTime در SQL Server بهره ببرید. برای مثال برای به دست آوردن فاصلهی میان دو روز از پرسوجوی زیر استفاده کنید:
Declare @a JalaliDate = '1392/02/07 00:00:00' Declare @b JalaliDate = '1392/02/05 00:00:00' SELECT DATEDIFF("DAY",@b.ToGregorianTime(),@a.ToGregorianTime()) AS DiffDate
شاد و پیروز باشید.
مروری بر کدهای کلاس SqlHelper
Public Shared Function GetPAGES() As List(Of EntityPAGES)
Dim cn As New SqlConnection(SiteHelper.GetConnectionString)
Dim cmd As New SqlCommand("GET_PAGES", cn) With {.CommandType = CommandType.StoredProcedure}
'cmd.Parameters.AddWithValue("", "")
Dim retlist As New List(Of EntityPAGES)()
Dim reader As SqlDataReader = Nothing
Try
cn.Open()
reader = cmd.ExecuteReader()
If reader.HasRows Then
Dim row As Integer = 1
Do While reader.Read()
Dim item As New EntityPAGES()
item.Division.Division_id = Integer.Parse(reader("Division_id").ToString())
item.Division.Name_persian = reader("DIVISION_NAME").ToString()
item.Page_id = Integer.Parse(reader("Page_id").ToString())
item.Page_no = Integer.Parse(reader("Page_no").ToString())
item.Masterpage.Masterpage_id = Integer.Parse(reader("Masterpage_id").ToString())
item.Page_file_name = reader("Page_file_name").ToString()
item.Page_title = reader("Page_title").ToString()
item.Page_link = reader("Page_link").ToString()
item.Page_delete = Boolean.Parse(reader("Page_delete").ToString())
item.Active = Boolean.Parse(reader("Active").ToString())
item.Remark = reader("Remark").ToString()
retlist.Add(item)
Loop
End If
Catch e1 As SqlException
Throw
Catch e2 As Exception
Throw
Finally
If reader IsNot Nothing Then
reader.Close()
End If
If cn.State <> ConnectionState.Closed Then
cn.Close()
cmd.Dispose()
End If
End Try
Return retlist
End Function
استاد مثلاً این کدی که من نوشتم اگه تعداد زیادی کاربر در حال DataEntry باشند اطلاعات اونها با هم قاطی میشه.
برخلاف تصور عموم، ساختار یک صفحه PDF شبیه به یک صفحه فایل Word نیست. این صفحات درحقیقت نوعی Canvas برای نقاشی هستند. در این بوم نقاشی، شکل، تصویر، متن و غیره در مختصات خاصی قرار خواهند گرفت. حتی کلمه «متن» میتواند به صورت سه حرف در سه مختصات خاص یک صفحه PDF نقاشی شود. برای درک بهتر این مورد نیاز است سورس یک صفحه PDF را بررسی کرد.
نحوه استخراج سورس یک صفحه PDF
using System.Diagnostics; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; namespace TestReaders { class Program { static void writePdf() { using (var document = new Document(PageSize.A4)) { var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create)); document.Open(); document.Add(new Paragraph("Test")); PdfContentByte cb = writer.DirectContent; BaseFont bf = BaseFont.CreateFont(); cb.BeginText(); cb.SetFontAndSize(bf, 12); cb.MoveText(88.66f, 367); cb.ShowText("ld"); cb.MoveText(-22f, 0); cb.ShowText("Wor"); cb.MoveText(-15.33f, 0); cb.ShowText("llo"); cb.MoveText(-15.33f, 0); cb.ShowText("He"); cb.EndText(); PdfTemplate tmp = cb.CreateTemplate(250, 25); tmp.BeginText(); tmp.SetFontAndSize(bf, 12); tmp.MoveText(0, 7); tmp.ShowText("Hello People"); tmp.EndText(); cb.AddTemplate(tmp, 36, 343); } Process.Start("test.pdf"); } private static void readPdf() { var reader = new PdfReader("test.pdf"); int intPageNum = reader.NumberOfPages; for (int i = 1; i <= intPageNum; i++) { byte[] contentBytes = reader.GetPageContent(i); File.WriteAllBytes("page-" + i + ".txt", contentBytes); } reader.Close(); } static void Main(string[] args) { writePdf(); readPdf(); } } }
اگر علاقمند باشید که سورس واقعی صفحات یک فایل PDF را مشاهده کنید، نحوه انجام آن توسط کتابخانه iTextSharp به صورت فوق است.
هرچند متد GetPageContent آرایهای از بایتها را بر میگرداند، اما اگر حاصل نهایی را در یک ادیتور متنی باز کنیم، قابل مطالعه و خواندن است. برای مثال، سورس مثال فوق (محتوای فایل page-1.txt تولید شده) به نحو زیر است:
q BT 36 806 Td 0 -18 Td /F1 12 Tf (Test)Tj 0 0 Td ET Q BT /F1 12 Tf 88.66 367 Td (ld)Tj -22 0 Td (Wor)Tj -15.33 0 Td (llo)Tj -15.33 0 Td (He)Tj ET q 1 0 0 1 36 343 cm /Xf1 Do Q
SaveGraphicsState(); // q BeginText(); // BT MoveTextPos(36, 806); // Td MoveTextPos(0, -18); // Td SelectFontAndSize("/F1", 12); // Tf ShowText("(Test)"); // Tj MoveTextPos(0, 0); // Td EndTextObject(); // ET RestoreGraphicsState(); // Q BeginText(); // BT SelectFontAndSize("/F1", 12); // Tf MoveTextPos(88.66, 367); // Td ShowText("(ld)"); // Tj MoveTextPos(-22, 0); // Td ShowText("(Wor)"); // Tj MoveTextPos(-15.33, 0); // Td ShowText("(llo)"); // Tj MoveTextPos(-15.33, 0); // Td ShowText("(He)"); // Tj EndTextObject(); // ET SaveGraphicsState(); // q TransMatrix(1, 0, 0, 1, 36, 343); // cm XObject("/Xf1"); // Do RestoreGraphicsState(); // Q
تا اینجا استخراج متن از فایلهای PDF ساده به نظر میرسد. باید به دنبال Tj گشت و حروف مرتبط با آنرا ذخیره کرد. اما در مورد «ترسیم» عبارات hello world و hello people اینطور نیست. عبارت hello world به حروف متفاوتی تقسیم شده و سپس در مختصات مشخصی ترسیم میگردد. عبارت hello people به صورت یک شیء ذخیره شده در قسمت منابع فایل PDF، بازیابی و نمایش داده میشود و اصلا در سورس صفحه جاری وجود ندارد.
این تازه قسمتی از نحوه عملکرد فایلهای PDF است. در فایلهای PDF میتوان قلمها را مدفون ساخت. همچنین این قلمها نیز تنها زیر مجموعهای از قلم اصلی مورد استفاده هستند. برای مثال اگر عبارت Test قرار است نمایش داده شود، فقط اطلاعات T، e و s در فایل نهایی PDF قرار میگیرند. به علاوه امکان تغییر کلی شماره Glyph متناظر با هر حرف نیز توسط PDF writer وجود دارد. به عبارتی الزامی نیست که مشخصات اصلی فونت حتما حفظ شود.
شاید بعضی از PDFهای فارسی را دیده باشید که پس از کپی متن آنها در برنامه Adobe reader و سپس paste آن در جایی دیگر، متن حاصل قابل خواندن نیست. علت این است که نحوه ذخیره سازی قلم مورد استفاده کاملا تغییر کرده است و برای بازیابی متن اینگونه فایلها، استفاده از OCR سادهترین روش است. برای نمونه در این قلم جدید مدفون شده، دیگر شماره کاراکتر 0x41 مساوی A نیست. بنابر سلیقه PDF writer این شماره به Glyph دیگری انتساب داده شده و چون قلم و مشخصات هندسی Glyph مورد استفاده در فایل PDF ذخیره میشود، برای نمایش این نوع فایلها هیچگونه مشکلی وجود ندارد. اما متن آنها به سادگی قابل بازیابی نیست.