Java 8 انقلابی در دسترسی به دادهها
آشنایی با JSON؛ ساده - خوانا - کم حجم
- تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار میگیرند. این فایل نیز در یک اسمبلی مستقل از برنامهی اصلی اضافه میشود.
- با استفاده از تزریق سرویس IStringLocalizer میتوان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامهی Web API دسترسی یافت.
- در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ میشود.
- کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx میباشند.
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی میکنیم.
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources
پس از ایجاد پروژهی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشهی جدید Resources را تعریف میکنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه میکنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.
کار با ین فایل (و یا فایلهای دیگری مانند SharedResource.en.resx) همانند تمام فایلهای منبع استاندارد است و نکتهی خاصی را به همراه ندارد.
معرفی فایل منبع اشتراکی به سرویسهای بومی سازی برنامه
پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی میکنیم:
public static class LocalizationConfig { public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services) { mvcBuilder.AddDataAnnotationsLocalization(options => { const string resourcesPath = "Resources"; string baseName = $"{resourcesPath}.{nameof(SharedResource)}"; var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name; options.DataAnnotationLocalizerProvider = (type, factory) => { // to use `SharedResource.fa.resx` file return factory.Create(baseName, location); }; }); services.AddLocalization(); services.AddScoped<IStringLocalizer>(provider => provider.GetRequiredService<IStringLocalizer<SharedResource>>()); services.AddScoped<ISharedResourceService, SharedResourceService>(); return mvcBuilder; } }
- سپس با استفاده از متد AddLocalization، سرویسهای پایهی بومی سازی ASP.NET Core به برنامه اضافه میشوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، میتوان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کردهایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت میکند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانههایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده میکنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده میکنید که در ادامهی بحث تکمیل خواهد شد.
هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار میگیرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddControllers().AddCustomLocalization(services); }
پس از آن نیاز است میانافزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام میدهد:
public static class LocalizationConfig { public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app) { var requestLocalizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") } }; app.UseRequestLocalization(requestLocalizationOptions); return app; } }
public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseCustomRequestLocalization(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
تعریف مدل برنامه به همراه ویژگیهای بومی سازی شده
در اینجا تعریف RegisterModel را مشاهده میکنید که ErrorMessageهای آن هرچند به ظاهر یک رشتهی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده میشوند:
using System.ComponentModel.DataAnnotations; namespace Core3xSharedResource.Models.Account { public class RegisterModel { [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources public string Email { get; set; } } }
فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?> <root> <data name="<b>Hello</b><i> {0}</i>" xml:space="preserve"> <value><b>سلام</b><i> {0}</i></value> </data> <data name="About Title" xml:space="preserve"> <value>درباره</value> </data> <data name="DNT" xml:space="preserve"> <value>.NET Tips</value> </data> <data name="SiteName" xml:space="preserve"> <value>DNT</value> </data> <data name="Please enter an email address" xml:space="preserve"> <value>لطفا ایمیلی را وارد کنید</value> </data> <data name="Please enter a valid email address" xml:space="preserve"> <value>لطفا ایمیل معتبری را وارد کنید</value> </data> </root>
آزمایش برنامه
اکنون برنامهی Web API، برای آزمایش آمادهاست. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازندهی کلاس تزریق شدهاست و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شدهی مدل برنامه را نیز بررسی میکنیم:
using Core3xSharedResource.Models.Account; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; namespace Core3xSharedResource.WebApi.Controllers { [ApiController] [Route("[controller]")] public class NormalIStringLocalizerController : ControllerBase { private readonly IStringLocalizer _localizer; public NormalIStringLocalizerController(IStringLocalizer localizer) { _localizer = localizer; } [HttpGet] public ActionResult<string> Get() { var localizedString = _localizer["About Title"]; if (localizedString.ResourceNotFound) { return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`."); } return localizedString.Value; } [HttpPost] public ActionResult<RegisterModel> Post(RegisterModel model) { return model; } } }
حالت get را در تصویر فوق مشاهده میکنید. در Web API برای تنظیم زبان مورد استفاده میتوان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شدهاست و نتیجهی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیشفرض آن همان fa-IR خواهد بود.
حالت post را نیز در تصویر زیر میتوان مشاهده کرد:
در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شدهی در مدل برنامه را دریافت کردهایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار میکند.
تعریف یک سرویس عمومی برای محصور سازی قابلیتهای بومی سازی، در برنامههای Web API
در ادامه تعریف سرویس SharedResourceService را مشاهده میکنید که ثبت آنرا پیشتر انجام دادیم:
using System; using System.Collections.Generic; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; namespace Core3xSharedResource.Services { public interface ISharedResourceService { string this[string index] { get; } IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures); string GetString(string name, params object[] arguments); string GetString(string name); } public class SharedResourceService : ISharedResourceService { private readonly IStringLocalizer _sharedLocalizer; private readonly ILogger<SharedResourceService> _logger; private readonly IHttpContextAccessor _httpContextAccessor; public SharedResourceService( IStringLocalizer sharedHtmlLocalizer, IHttpContextAccessor httpContextAccessor, ILogger<SharedResourceService> logger ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { return _sharedLocalizer.GetAllStrings(includeParentCultures); } public string this[string index] => GetString(index); public string GetString(string name, params object[] arguments) { var result = _sharedLocalizer.GetString(name, arguments); logError(name, result); return result; } private void logError(string name, LocalizedString result) { if (result.ResourceNotFound) { var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"]; _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`."); } } public string GetString(string name) { var result = _sharedLocalizer.GetString(name); logError(name, result); return result; } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Core3xSharedResource.zip
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
فعالسازی Windows Authentication در IIS
پس از publish برنامه و رعایت مواردی که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بحث شد، باید به قسمت Authentication برنامهی مدنظر، در کنسول مدیریتی IIS رجوع کرد:
و سپس Windows Authentication را با کلیک راست بر روی آن و انتخاب گزینهی Enable، فعال نمود:
این تنظیم دقیقا معادل افزودن تنظیمات ذیل به فایل web.config برنامه است:
<system.webServer> <security> <authentication> <anonymousAuthentication enabled="true" /> <windowsAuthentication enabled="true" /> </authentication> </security> </system.webServer>
فعالسازی Windows Authentication در IIS Express
اگر برای آزمایش میخواهید از IIS Express به همراه ویژوال استودیو استفاده کنید، نیاز است فایلی را به نام Properties\launchSettings.json با محتوای ذیل در ریشهی پروژهی خود ایجاد کنید (و یا تغییر دهید):
{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:3381/", "sslPort": 0 } } }
تغییر مهم فایل web.config برنامه جهت هدایت اطلاعات ویندوز به آن
اگر پروژهی شما فایل web.config ندارد، باید آنرا اضافه کنید؛ با حداقل محتوای ذیل:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/> </system.webServer> </configuration>
تنظیمات برنامهی ASP.NET Core جهت فعالسازی Windows Authentication
پس از فعالسازی windowsAuthentication در IIS و همچنین تنظیم forwardWindowsAuthToken به true در فایل web.config برنامه، اکنون جهت استفادهی از windowsAuthentication دو روش وجود دارد:
الف) تنظیمات مخصوص برنامههای Self host
اگر برنامهی وب شما قرار است به صورت self host ارائه شود (بدون استفاده از IIS)، تنها کافی است در تنظیمات ابتدای برنامه در فایل Program.cs، استفادهی از میانافزار HttpSys را ذکر کنید:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup<Startup>() .UseHttpSys(options => // Just for local tests without IIS, Or self-hosted scenarios on Windows ... { options.Authentication.Schemes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM; options.Authentication.AllowAnonymous = true; //options.UrlPrefixes.Add("http://+:80/"); }) .Build(); host.Run(); } } }
ب) تنظیمات مخصوص برنامههایی که قرار است در IIS هاست شوند
در اینحالت تنها کافی است UseIISIntegration در تنظیمات ابتدایی برنامه ذکر شود و همانطور که عنوان شد، نیازی به UseHttpSys در این حالت نیست:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build(); host.Run(); } } }
فعالسازی میانافزار اعتبارسنجی ASP.NET Core جهت یکپارچه شدن با Windows Authentication
در پایان تنظیمات فعالسازی Windows Authentication نیاز است به فایل Startup.cs برنامه مراجعه کرد و یکبار AddAuthentication را به همراه تنظیم ChallengeScheme آن به IISDefaults افزود:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.Configure<IISOptions>(options => { // Sets the HttpContext.User // Note: Windows Authentication must also be enabled in IIS for this to work. options.AutomaticAuthentication = true; options.ForwardClientCertificate = true; }); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; }); }
آزمایش برنامه با تدارک یک کنترلر محافظت شده
در اینجا قصد داریم اطلاعات ذیل را توسط تعدادی اکشن متد، نمایش دهیم:
private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; }
namespace ASPNETCore2WindowsAuthentication.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } [Authorize] public IActionResult Windows() { return Content(authInfo()); } private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; } [AllowAnonymous] public IActionResult Anonymous() { return Content(authInfo()); } [Authorize(Roles = "Domain Admins")] public IActionResult ForAdmins() { return Content(authInfo()); } [Authorize(Roles = "Domain Users")] public IActionResult ForUsers() { return Content(authInfo()); } } }
dotnet publish
اکنون اگر برنامه را در مرورگر مشاهده کنیم، یک چنین خروجی قابل دریافت است:
در اینجا نام کاربر وارد شدهی به ویندوز و همچنین لیست تمام Claims او مشاهده میشوند. مسیر Home/Windows نیز توسط ویژگی Authorize محافظت شدهاست.
برای محدود کردن دسترسی کاربران به اکشن متدها، توسط گروههای دومین و اکتیودایرکتوری، میتوان به نحو ذیل عمل کرد:
[Authorize(Roles = @"<domain>\<group>")] //or [Authorize(Roles = @"<domain>\<group1>,<domain>\<group2>")]
services.AddAuthorization(options => { options.AddPolicy("RequireWindowsGroupMembership", policy => { policy.RequireAuthenticatedUser(); policy.RequireRole(@"<domain>\<group>")); } });
[Authorize(Policy = "RequireWindowsGroupMembership")]
[HttpGet("[action]")] public IActionResult SomeValue() { if (!User.IsInRole(@"Domain\Group")) return StatusCode(403); return Ok("Some Value"); }
افزودن Claims سفارشی به Claims پیشفرض کاربر سیستم
همانطور که در شکل فوق ملاحظه میکنید، یک سری Claims حاصل از Windows Authentication در اینجا به شیء User اضافه شدهاند؛ بدون اینکه برنامه، صفحهی لاگینی داشته باشد و همینقدر که کاربر به ویندوز وارد شدهاست، میتواند از برنامه استفاده کند.
اگر نیاز باشد تا Claims خاصی به لیست Claims کاربر جاری اضافه شود، میتوان از پیاده سازی یک IClaimsTransformation سفارشی استفاده کرد:
public class ApplicationClaimsTransformation : IClaimsTransformation { private readonly ILogger<ApplicationClaimsTransformation> _logger; public ApplicationClaimsTransformation(ILogger<ApplicationClaimsTransformation> logger) { _logger = logger; } public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { if (!(principal.Identity is ClaimsIdentity identity)) { return Task.FromResult(principal); } var claims = addExistingUserClaims(identity); identity.AddClaims(claims); return Task.FromResult(principal); } private IEnumerable<Claim> addExistingUserClaims(IIdentity identity) { var claims = new List<Claim>(); var user = @"VahidPC\Vahid"; if (identity.Name != user) { _logger.LogError($"Couldn't find {identity.Name}."); return claims; } claims.Add(new Claim(ClaimTypes.GivenName, user)); return claims; } }
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>(); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; });
به این ترتیب میتوان لیست Claims ثبت شدهی یک کاربر را در یک بانک اطلاعاتی استخراج و به لیست Claims فعلی آن افزود و دسترسیهای بیشتری را به او اعطاء کرد (فراتر از دسترسیهای پیشفرض سیستم عامل).
برای دسترسی به مقادیر این Claims نیز میتوان به صورت ذیل عمل کرد:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var userName = User.FindFirstValue(ClaimTypes.Name); var userName = User.FindFirstValue(ClaimTypes.GivenName);
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید: ASPNETCore2WindowsAuthentication.zip
در قسمت قبل « کار با اسکنر در برنامههای تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژهی عملی و پیاده سازی مطالب مطرح شده میرویم.
ابتدا پروژهی WCF را شروع میکنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژهی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :
[ServiceContract] public interface IScannerService { [OperationContract] [WebInvoke(Method = "GET", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, UriTemplate = "GetScan")] string GetScan(); }
public class ScannerService : IScannerService { public string GetScan() { // TODO Add code here } }
بر روی پروژهی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینهی Microsoft Windows Image Acquisition Library v2.0 را به پروژهی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر میشود که ارجاعی از آن را در کلاس ScannerService قرار میدهیم.
using WIA;
public string GetScan() { var imgResult = String.Empty; var dialog = new CommonDialogClass(); try { // نمایش فرم پیشفرض اسکنر var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType); // ذخیره تصویر در یک فایل موقت var filename = Path.GetTempFileName(); image.SaveFile(filename); var img = Image.FromFile(filename); // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg); } catch { // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی // بازگردانده میشود که به معنای نبود تصویر میباشد } return imgResult; }
CommonDialogClass کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر میباشد و متدهای مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار میدهد که بسته به نیاز خود میتوانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage میباشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش میدهد و کاربر از طریق آن میتواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :
و سپس فرم پیش فرض اسکنرهای TWAIN را جهت تعیین تنظیمات اسکن نمایش میدهد که این امکان نیز در این فرم فراهم است تا دستگاههای Feeder یا Flated انتخاب گردند.
خروجی این متد همان عکس اسکن شده است که از نوع WIA.ImageFile میباشد و ما پس از دریافتش، ابتدا آن را در یک فایل موقت ذخیره نموده و سپس با استفاده از یک متد کمکی آن را به فرمت Base64 برای درخواست کننده اسکن ارسال مینماییم.
کدهای کلاس کمکی ImageHelper:
public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format) { if (image != null) { using (MemoryStream ms = new MemoryStream()) { // Convert Image to byte[] image.Save(ms, format); byte[] imageBytes = ms.ToArray(); // Convert byte[] to Base64 String string base64String = Convert.ToBase64String(imageBytes); return base64String; } } return String.Empty; }
این مثال به سادهترین شکل نوشته شد. کلاس دیگری هم در اینجا وجود دارد و در صورتیکه از اسکنر نوع Feeder استفاده میکنید، میتوانید از کدهای آن استفاده کنید.
جهت رفع این خطا، در قسمت Referenceهای پروژه خود، WIA را انتخاب نموده و از Propertiesهای آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل میشود.
به سراغ پروژهی ویندوز فرم جهت هاست کردن این WCF سرویس میرویم. میتوانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده میکنیم.
یک پروژهی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژهی قبلی رفته و dllهای آن را به پروژه خود اضافه نمایید.
Form1.cs را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
private readonly Uri _baseAddress = new Uri("http://localhost:6019"); private ServiceHost _host;
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه مینماییم:
private void Form1_Load(object sender, EventArgs e) { _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress); _host.Open(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _host.Close(); }
فایل App.Config پروژهی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="BehaviourMetaData"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="WcfServiceScanner.ScannerService" behaviorConfiguration="BehaviourMetaData"> <endpoint address="" binding="basicHttpBinding" contract="WcfServiceScanner.IScannerService" /> </service> </services> </system.serviceModel> </configuration>
اگر موفق به اجرا نشدید و احیانا با خطای زیر مواجه شدید، اطمینان حاصل کنید که ویژوال استودیو Run as Administrator باشد. مشکل حل خواهد شد.
به سراغ پروژهی بعدی، یعنی وب سایت خود میرویم. یک پروژهی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه میکنیم.
( از آنجا که کدها به صورت جاوا اسکریپت میباشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحهی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a> <img src="" id="img-scanned" /> <script> $("#get-scan").click(function () { var url = 'http://localhost:6019/'; $.get(url, function (data) { $("#img-scanned").attr("src","data:image/Jpeg;base64, "+ data.GetScanResult); }); }); </script>
راه حلهای زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژهی ما جوابگو نمیباشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژهی WCF Service میباشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
public class CORSSupport : IDispatchMessageInspector { Dictionary<string, string> requiredHeaders; public CORSSupport(Dictionary<string, string> headers) { requiredHeaders = headers ?? new Dictionary<string, string>(); } public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty; if (httpRequest.Method.ToLower() == "options") instanceContext.Abort(); return httpRequest; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty; var httpRequest = correlationState as HttpRequestMessageProperty; foreach (var item in requiredHeaders) { httpResponse.Headers.Add(item.Key, item.Value); } var origin = httpRequest.Headers["origin"]; if (origin != null) httpResponse.Headers.Add("Access-Control-Allow-Origin", origin); var method = httpRequest.Method; if (method.ToLower() == "options") httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent; } } // Simply apply this attribute to a DataService-derived class to get // CORS support in that service [AttributeUsage(AttributeTargets.Class)] public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior { #region IServiceBehavior Members void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { var requiredHeaders = new Dictionary<string, string>(); //Chrome doesn't accept wildcards when authorization flag is true //requiredHeaders.Add("Access-Control-Allow-Origin", "*"); requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS"); requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type"); requiredHeaders.Add("Access-Control-Allow-Credentials", "true"); foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher ed in cd.Endpoints) { ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders)); } } } void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } #endregion }
using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher;
[CORSSupportBehavior] public class ScannerService : IScannerService {
کار تمام است، یکبار دیگر ابتدا پروژهی WcfServiecScanner و سپس پروژه هاست را Build کرده و برنامهی هاست را اجرا کنید. اکنون مشاهده میکنید که با زدن دکمهی اسکن، اسکنر فرم تنظیمات اسکن را نمایش میدهد که پس از زدن دکمهی Scan، پروسه آغاز شده و پس از اتمام، تصویر اسکن شده در صفحهی وب سایت نمایش داده میشود.
کتابخانه جایگزین آنرا افزونه XMLWorker معرفی کردهاند که توانایی پردازش CSS و HTML بهتر و کاملتری را نسبت به HTMLWorker ارائه میدهد. این کتابخانه نیز همانند HTMLWorker پشتیبانی توکاری از متون راست به چپ و یونیکد فارسی، ندارد و نیاز است برای نمایش صحیح متون فارسی در آن، نکات خاصی را اعمال نمود که در ادامه بحث آنها را مرور خواهیم کرد.
ابتدا برای دریافت آخرین نگارشهای iTextSharp و افزونه XMLWorker آن به آدرسهای ذیل مراجعه نمائید:
تهیه یک UnicodeFontProvider
Encoding پیش فرض قلمها در XMLWorker مساوی BaseFont.CP1252 است؛ که از حروف یونیکد پشتیبانی نمیکند. برای رفع این نقیصه نیاز است یک منبع تامین قلم سفارشی را برای آن ایجاد نمود:
public class UnicodeFontProvider : FontFactoryImp { static UnicodeFontProvider() { // روش صحیح تعریف فونت var systemRoot = Environment.GetEnvironmentVariable("SystemRoot"); FontFactory.Register(Path.Combine(systemRoot, "fonts\\tahoma.ttf")); // ثبت سایر فونتها در اینجا //FontFactory.Register(Path.Combine(Environment.CurrentDirectory, "fonts\\irsans.ttf")); } public override Font GetFont(string fontname, string encoding, bool embedded, float size, int style, BaseColor color, bool cached) { if (string.IsNullOrWhiteSpace(fontname)) return new Font(Font.FontFamily.UNDEFINED, size, style, color); return FontFactory.GetFont(fontname, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, size, style, color); } }
مابقی مسایل آن خودکار خواهد بود و هر زمانیکه نیاز به قلم خاصی از طرف XMLWorker وجود داشت، به متد GetFont فوق مراجعه کرده و اینبار قلمی با BaseFont.IDENTITY_H را دریافت میکند. IDENTITY_H در استاندارد PDF، جهت مشخص ساختن encoding قلمهایی با پشتیبانی از یونیکد بکار میرود.
تهیه منبع تصاویر
در XMLWorker اگر تصاویر با http شروع نشوند (دریافت تصاویر وب آن خودکار است)، آن تصاویر را از مسیری که توسط پیاده سازی کلاس AbstractImageProvider مشخص خواهد شد، دریافت میکند که نمونهای از پیاده سازی آنرا در ذیل مشاهده میکنید:
public class ImageProvider : AbstractImageProvider { public override string GetImageRootPath() { var path = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); return path + "\\"; // مهم است که این مسیر به بک اسلش ختم شود تا درست کار کند } }
نحوه تعریف یک فایل CSS خارجی
public static class XMLWorkerUtils { /// <summary> /// نحوه تعریف یک فایل سی اس اس خارجی /// </summary> public static ICssFile GetCssFile(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { return XMLWorkerHelper.GetCSS(stream); } } }
تبدیل المانهای HTML پردازش شده به یک لیست PDF ایی
تهیه مقدمات فارسی سازی و نمایش راست به چپ اطلاعات در کتابخانه XMLWorker از اینجا شروع میشود. در حالت پیش فرض کار آن، المانهای HTML به صورت خودکار Parse شده و به صفحه اضافه میشوند. به همین دلیل دیگر فرصت اعمال خواص RTL به المانهای پردازش شده دیگر وجود نخواهد داشت و به صورت توکار نیز این مسایل درنظر گرفته نمیشود. به همین دلیل نیاز است که در حین پردازش المانهای HTML و تبدیل آنها به معادل المانهای PDF، بتوان آنها را جمع آوری کرد که نحوه انجام آنرا با پیاده سازی اینترفیس IElementHandler در ذیل مشاهده میکنید:
/// <summary> /// معادل پی دی افی المانهای اچ تی ام ال را جمع آوری میکند /// </summary> public class ElementsCollector : IElementHandler { private readonly Paragraph _paragraph; public ElementsCollector() { _paragraph = new Paragraph { Alignment = Element.ALIGN_LEFT // سبب میشود تا در حالت راست به چپ از سمت راست صفحه شروع شود }; } /// <summary> /// این پاراگراف حاوی کلیه المانهای متن است /// </summary> public Paragraph Paragraph { get { return _paragraph; } } /// <summary> /// بجای اینکه خود کتابخانه اصلی کار افزودن المانها را به صفحات انجام دهد /// قصد داریم آنها را ابتدا جمع آوری کرده و سپس به صورت راست به چپ به صفحات نهایی اضافه کنیم /// </summary> /// <param name="htmlElement"></param> public void Add(IWritable htmlElement) { var writableElement = htmlElement as WritableElement; if (writableElement == null) return; foreach (var element in writableElement.Elements()) { fixNestedTablesRunDirection(element); _paragraph.Add(element); } } /// <summary> /// نیاز است سلولهای جداول تو در توی پی دی اف نیز راست به چپ شوند /// </summary> private void fixNestedTablesRunDirection(IElement element) { var table = element as PdfPTable; if (table == null) return; table.RunDirection = PdfWriter.RUN_DIRECTION_RTL; foreach (var row in table.Rows) { foreach (var cell in row.GetCells()) { cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL; foreach (var item in cell.CompositeElements) { fixNestedTablesRunDirection(item); } } } } }
یک مثال کامل از نحوه کنار هم قرار دادن پیشنیازهای تهیه شده
خوب؛ تا اینجا یک سری پیشنیاز را تهیه کردیم، اما XMLWorker از وجود آنها بیخبر است. برای معرفی آنها باید به نحو ذیل عمل کرد:
using (var pdfDoc = new Document(PageSize.A4)) { var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("test.pdf", FileMode.Create)); pdfWriter.RgbTransparencyBlending = true; pdfDoc.Open(); var html = @"<span style='color:blue; font-family:tahoma;'><b>آزمایش</b></span> کتابخانه <i>iTextSharp</i> <u>جهت بررسی فارسی نویسی</u> <table style='color:blue; font-family:tahoma;' border='1'><tr><td>eeمتن</td></tr></table> <code>This is a code!</code> <br/> <img src='av-13489.jpg' /> "; var cssResolver = new StyleAttrCSSResolver(); // cssResolver.AddCss(XMLWorkerUtils.GetCssFile(@"c:\path\pdf.css")); cssResolver.AddCss(@"code { padding: 2px 4px; color: #d14; white-space: nowrap; background-color: #f7f7f9; border: 1px solid #e1e1e8; }", "utf-8", true); // کار جمع آوری المانهای ترجمه شده به المانهای پی دی اف را انجام میدهد var elementsHandler = new ElementsCollector(); var htmlContext = new HtmlPipelineContext(new CssAppliersImpl(new UnicodeFontProvider())); htmlContext.SetImageProvider(new ImageProvider()); htmlContext.CharSet(Encoding.UTF8); htmlContext.SetAcceptUnknown(true).AutoBookmark(true).SetTagFactory(Tags.GetHtmlTagProcessorFactory()); var pipeline = new CssResolverPipeline(cssResolver, new HtmlPipeline(htmlContext, new ElementHandlerPipeline(elementsHandler, null))); var worker = new XMLWorker(pipeline, parseHtml: true); var parser = new XMLParser(); parser.AddListener(worker); parser.Parse(new StringReader(html)); // با هندلر سفارشی که تهیه کردیم تمام المانهای اچ تی ام ال به المانهای پی دی اف تبدیل شدند // الان تنها کافی کافی است تا اینها را در یک جدول راست به چپ محصور کنیم تا درست نمایش داده شوند var mainTable = new PdfPTable(1) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL }; var cell = new PdfPCell { Border = 0, RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT }; cell.AddElement(elementsHandler.Paragraph); mainTable.AddCell(cell); pdfDoc.Add(mainTable); } Process.Start("test.pdf");
UnicodeFontProvider باید به HtmlPipelineContext شناسانده شود.
ImageProvider توسط متد SetImageProvider به HtmlPipelineContext معرفی میشود.
ElementsCollector سفارشی ما در قسمت CssResolverPipeline باید به سیستم تزریق شود.
پس از آن XMLWorker را وادار میکنیم تا HTML را Parse کرده و معادل المانهای PDF ایی آنرا تهیه کند؛ اما آنها را به صورت خودکار به صفحات فایل PDF نهایی اضافه نکند. در این بین ElementsCollector ما این المانها را جمع آوری کرده و در نهایت، پاراگراف کلی حاصل از آنرا به یک جدول با RUN_DIRECTION_RTL اضافه میکنیم. حاصل آن نمایش صحیح متون فارسی است.
کدهای مثال فوق را از آدرس ذیل نیز میتوانید دریافت کنید:
XMLWorkerRTLsample.cs
به روز رسانی
کلیه نکات مطلب فوق را به همراه بهبودهای مطرح شده در نظرات آن، در پروژهی ذیل میتوانید به صورت یکجا دریافت و بررسی کنید:
XMLWorkerRTLsample.zip