claims.Add(new Claim(UserClaimTypes.PackedPermission, new[] {"48", "65", "6C", "6C", "6F", "20", "57", "6F", "72", "6C", "64", "21"} .PackPermissionsToString()));
برای نمونه در اینجا توسط متد inMemoryFile، یک فایل PDF در حافظه تشکیل شده و سپس به صورت یک Byte Array بازگشت داده میشود. در ادامه کار، این اطلاعات در مرورگر کاربر Flush خواهد شد:
using System.IO; using System.Net.Mime; using System.Web; namespace WebApplication { public class PdfHandler : IHttpHandler { private static byte[] inMemoryFile() { //تولید پویای فایل در حافظه و یا حتی خواندن از یک نمونه موجود return File.ReadAllBytes(@"D:\path\DynamicCrosstabSampleRpt.pdf"); } public void ProcessRequest(HttpContext context) { var pdf = inMemoryFile(); context.Response.Cache.SetCacheability(HttpCacheability.NoCache); context.Response.ContentType = MediaTypeNames.Application.Pdf; context.Response.AddHeader("Content-Length", pdf.Length.ToString()); context.Response.AddHeader("content-disposition", "attachment;filename=test.pdf"); context.Response.Buffer = true; context.Response.Clear(); context.Response.OutputStream.Write(pdf, 0, pdf.Length); context.Response.OutputStream.Flush(); context.Response.OutputStream.Close(); context.Response.End(); } public bool IsReusable { get { return false; } } } }
<iframe width="100%" src="PdfHandler.ashx" height="200px"></iframe>
سؤال: فرض کنید Adobe reader بر روی سیستم نصب است و مرورگر با استفاده از Active-X آن میتواند این نوع فایلها را نمایش دهد. آیا راهی وجود دارد تا بجای نمایش save popup dialog، این فایل توسط مرورگر نمایش داده شود؟
پاسخ: بلی. در کدهای فوق تنها کافی است یک سطر آن تغییر کند:
Response.AddHeader("content-disposition", "inline;filename=test.pdf");
اینبار اگر برنامه را اجرا کنیم، iframe ایی که به PdfHandler.ashx اشاره میکند، فایل PDF را در صفحه نمایش میدهد.
کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشینهای حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا 30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و سادهای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویسها سادهتر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.
دریافت کتابخانه Stateless از Google code و یا از NuGet
پیاده سازی مثال کلید برق با Stateless
در ادامه همان مثال ساده کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
using System; using Stateless; namespace StatelessTests { class Program { static void Main(string[] args) { try { string on = "On", off = "Off"; var space = ' '; var onOffSwitch = new StateMachine<string, char>(initialState: off); onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on); onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off); Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error."); while (true) { Console.WriteLine("Switch is in state: " + onOffSwitch.State); var pressed = Console.ReadKey(true).KeyChar; onOffSwitch.Fire(trigger: pressed); } } catch (Exception ex) { Console.WriteLine("Exception: " + ex.Message); Console.WriteLine("Press any key to continue..."); Console.ReadKey(true); } } } }
امضای کلاس StateMachine را در ذیل مشاهده میکنید؛ جهت توضیح آرگومانهای جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
برای مثال در اینجا حالات روشن و خاموش، با رشتههای on و off مشخص شدهاند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن میشوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص میکند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بینهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز میتواند همین کار را در برنامههای ویندوزی انجام دهد.
یک نکته
علاوه بر روشهای یاد شدهی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز میتوان به این لیست افزود.
استفاده از Stateless Designer برای تولید کدهای ماشین حالت
کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آنرا به شکل افزونهای برای VS.NET میتوانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده میکند.
کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع میشود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema"> <settings> <itemname>BlogPostStateMachine</itemname> <namespace>StatelessTests</namespace> <class>public</class> </settings> <triggers> <trigger>Save</trigger> <trigger>RequireEdit</trigger> <trigger>Accept</trigger> <trigger>Reject</trigger> </triggers> <states> <state start="yes">Begin</state> <state>InProgress</state> <state>Published</state> <state>Rejected</state> </states> <transitions> <transition trigger="Save" from="Begin" to="InProgress" /> <transition trigger="Accept" from="InProgress" to="Published" /> <transition trigger="Reject" from="InProgress" to="Rejected" /> <transition trigger="Save" from="InProgress" to="InProgress" /> <transition trigger="RequireEdit" from="Published" to="InProgress" /> <transition trigger="RequireEdit" from="Rejected" to="InProgress" /> </transitions> </statemachine>
به علاوه کدهای زیر که به صورت خودکار تولید شدهاند:
using Stateless; namespace StatelessTests { public class BlogPostStateMachine { public delegate void UnhandledTriggerDelegate(State state, Trigger trigger); public delegate void EntryExitDelegate(); public delegate bool GuardClauseDelegate(); public enum Trigger { Save, RequireEdit, Accept, Reject, } public enum State { Begin, InProgress, Published, Rejected, } private readonly StateMachine<State, Trigger> stateMachine = null; public EntryExitDelegate OnBeginEntry = null; public EntryExitDelegate OnBeginExit = null; public EntryExitDelegate OnInProgressEntry = null; public EntryExitDelegate OnInProgressExit = null; public EntryExitDelegate OnPublishedEntry = null; public EntryExitDelegate OnPublishedExit = null; public EntryExitDelegate OnRejectedEntry = null; public EntryExitDelegate OnRejectedExit = null; public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null; public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null; public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null; public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null; public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null; public UnhandledTriggerDelegate OnUnhandledTrigger = null; public BlogPost() { stateMachine = new StateMachine<State, Trigger>(State.Begin); stateMachine.Configure(State.Begin) .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); }) .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); }) .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.InProgress) .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); }) .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); }) .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } ) .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } ) .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } ) ; stateMachine.Configure(State.Published) .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); }) .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.Configure(State.Rejected) .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); }) .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); }) .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } ) ; stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); }); } public bool TryFireTrigger(Trigger trigger) { if (!stateMachine.CanFire(trigger)) { return false; } stateMachine.Fire(trigger); return true; } public State GetState { get { return stateMachine.State; } } } }
ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شدهاند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved میتوان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرطها ایجاد کرده است، میتوان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت میگردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.
نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشینهای حالت، refactoing انبوهی از if و elseها است که در حالت مدیریت یک چنین گردشهای کاری باید تدارک دید.
namespace StatelessTests { public class BlogPostManager { private BlogPostStateMachine _stateMachine; public BlogPostManager() { configureWorkflow(); } private void configureWorkflow() { _stateMachine = new BlogPostStateMachine(); _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; }; _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; }; _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; }; _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ }; _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ }; _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; }; } public bool UserIsAdmin { get { return true; // TODO: Evaluate if user is an admin. } } public bool UserCanPost { get { return true; // TODO: Evaluate if user is authenticated } } public bool UserHasEditRights { get { return true; // TODO: Evaluate if user is owner or admin } } // User actions public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); } public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); } // Admin actions public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); } public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); } } }
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز میباشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه میکنید. کد نویسی آنها در حد فراخوانی متد TryFireTrigger ماشین حالت است.
یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. میتوان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحلهای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیمگیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
کتابخانه جایگزین آنرا افزونه 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
ابتدا یک پروژه با دو Console Application با نام های Service و Client ایجاد کنید. سپس در پروژه Service یک سرویس به نام BookService ایجاد کنید و کدهای زیر را در آن کپی نمایید:
Contract مربوطه به صورت زیر است:
[ServiceContract] public interface IBookService { [OperationContract] int GetCountOfBook(); }
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] public class BookService : IBookService { public int GetCountOfBook() { return 10; } }
class Program { static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(BookService)); var binding = new BasicHttpBinding(); host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService"); host.Open(); Console.Write("BookService host"); Console.ReadKey(); } }
حال نوبت به پیاده سازی سمت کلاینت میرسد. فایل Program سمت کلاینت را باز کرده و کدهای زیر را نیز در آن کپی نمایید:
static void Main(string[] args) { Thread.Sleep(2000); BasicHttpBinding binding = new BasicHttpBinding(); ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService")); Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook()); Console.ReadKey(); }
خروجی زیر مشاهده میشود:
تا اینجا هیچ گونه اعتبار سنجی انجام نشد. برای پیاده سازی اعتبار سنجی باید یک سری تنظیمات بر روی Binding و Hosting سمت سرور و البته کلاینت بر قرار شود. فایل Program پروزه Service را باز نمایید و محتویات آن را به صورت زیر تغییر دهید:
static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(BookService)); var binding = new BasicHttpBinding(); binding.Security = new BasicHttpSecurity(); binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom; host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator(); host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService"); host.Open(); Console.Write("BookService host"); Console.ReadKey(); }
ابتدا نوع Security در Binding را به حالت TransportCredentialOnly تنظیم کردیم. در یک جمله هیچ گونه تضمینی برای صحت اطلاعات انتقالی در این حالت وجود ندارد و فقط یک اعتبار سنجی اولیه انجام خواهد شد. در نتیجه هنگام استفاده از این حالت باید با دقت عمل نمود و نباید فقط به پیاده سازی این حالت اکتفا کرد.( Encryption اطلاعات سرویسها مورد بحث این پست نیست)
ClientCredentialType نیز باید به حالت Basic تنظیم شود. در WCF اعتبار سنجی به صورت پیش فرض در حالت Windows است (بعنی UserNamePasswordValidationMode برابر مقدار Windows است و اعتبار سنجی بر اساس کاربر انجام میشود) . این مورد باید به مقدار Custom تغییر یابد. در انتها نیز باید مدل اعتبار سنجی دلخواه خود را به صورت زیر پیاده سازی کنیم:
در پروژه سرویس یک کلاس به نام CustomUserNamePasswordValidator بسازید و کدهای زیر را در آن کپی کنید:
public class CustomUserNamePasswordValidator : UserNamePasswordValidator { public override void Validate(string userName, string password) { if (userName != "Masoud" || password != "Pakdel") throw new SecurityException("Incorrect userName or password"); } }
تغییرات مورد نیاز سمت کلاینت:
اگر در این حالت پروژه را اجرا نمایید از آن جا که از این به بعد، درخواستها سمت سرور اعتبار سنجی میشوند در نتیجه با خطای زیر روبرو خواهید شد:
این خطا از آن جا ناشی میشود که تنظیمات کلاینت و سرور از نظر امنیتی با هم تناسب ندارد. در نتیجه باید تنظیمات Binding کلاینت و سرور یکی شود. برای این کار کد زیر را به فایل Program سمت کلاینت اضافه میکنیم:
static void Main(string[] args) { Thread.Sleep(2000); BasicHttpBinding binding = new BasicHttpBinding(); binding.Security = new BasicHttpSecurity(); binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService")); channel.Credentials.UserName.UserName = "WrongUserName"; channel.Credentials.UserName.Password = "WrongPassword";
Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook()); Console.ReadKey(); }
channel.Credentials.UserName.UserName = "WrongUserName"; channel.Credentials.UserName.Password = "WrongPassword";
دریافت سورس مثال بالا
این روش دارای تمام خواص یک تراکنش است(اصطلاحا به این خواص ACID Properties گفته میشود)
1-Atomic : به این معناست که تمام دستورات بین بلاک (دستورات SQL و سایر عملیات) باید به صورت عملیات اتمی کار کنند. یعنی یا تمام عملیات موفقیت آمیز است یا همه با شکست روبرو میشوند.
2 - Consistent: به این معناست که اگر تراکنش موفقیت آمیز بود پایگاه داده باید در شروع تراکنش بعدی تغییرات لازم رو انجام داده باشد و در غیر این صورت پایگاه داده باید به حالت قبل از شروع تراکنش برگردد.
3- Isolated: اگر چند تا تراکنش هم زمان شروع شوند اجرای هیچ کدوم از اونها نباید بر اجرای بقیه تاثیر بزاره.
4- Durable: یعنی تغییرات حاصل شده بعد از اتمام تراکنش باید دائمی باشند.
روش کار به این صورت است تمام کارهایی که قصد داریم در طی یک تراکنش انجام شوند باید در یک بلاک قرار بگیرند و تا زمانی که متد Complete فراخوانی شود. در این بلاک شما هر عملیاتی رو که به عنوان جزئی از تراکنش میدونید قرار بدید. در صورتی که کنترل اجرا به فراخوانی دستور Complete برسه تمام موارد قبل از این دستور Commit میشوند در غیر این صورت RollBack.
به مثال زیر دقت کنید.
ابتدا به پروژه مربوطه باید اسمبلی System.Transaction رو اضافه کنید.
using ( TransactionScope scope = new TransactionScope() ) { //Statement1 //Statement2 //Statement3 scope.Complete(); }
using ( System.Transactions.TransactionScope scope = new System.Transactions.TransactionScope() ) { if ( result == 0 ) { throw new ApplicationException(); } scope.Complete(); }
نکته 1: میزان Timeout در این تراکنشها چه قدر است؟
برای بدست آوردن مقدار Timeout در این گونه تراکنشها میتوانید از کلاس TransactionManager استفاده کنید. به صورت زیر :
var defaultTimeout = TransactionManager.DefaultTimeout var maxTimeout = TransactionManager.MaximumTimeout
TransactionOptions option = new TransactionOptions(); option.Timeout = TimeSpan.MaxValue;
using ( System.Transactions.TransactionScope scope = new System.Transactions.TransactionScope(TransactionScopeOption.Required ,option) ) { scope.Complete(); }
1 - Required : یعنی نیاز به تراکنش وجود دارد. در صورتی که تراکنش در یک تراکنش دیگر شروع شود نیاز به ساختن تراکنش جدید نیست و از همان تراکنش قبلی برای این کار استفاده میشود.
2 - RequiresNew: در هر صورت برای محدوده یک تراکنش تولید میشود.
3- Suppress : به عنوان محدوه تراکنش در نظر گرفته نمیشود.
using(TransactionScope scope1 = new TransactionScope()) { try { using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress)) { //به دلیل استفاده از Suppress این محدوده خارج از تراکنش محسوب میشود } //شروع محدوده تراکنش } catch {} //Rest of scope1 }
1-این روش از تراکنشهای توزیع شده پشتیبانی میکند . یعنی میتونید از چند تا منبع داده استفاده کنید یا میتونید از یک تراکنش چند تا Connection به یک منبع داده باز کنید.(استفاده از چند تاconnection در طی یک تراکنش)
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required)) { string strCmd = "SQL to Execute"; conn = new SqlClient.SqlConnection("Connection to DB1"); conn.Open() objCmd = new SqlClient.SqlCommand(strCmd, conn); objCmd.ExecuteNonQuery(); string strCmd2 = "SQL to Execute"; conn2 = new SqlClient.SqlConnection("Connection to DB2"); conn2.Open() objCmd2 = new SqlClient.SqlCommand(strCmd2, conn2); objCmd2.ExecuteNonQuery(); }
3- با DataProviderهای متفاوت نظیر Oracle و OleDb و ODBC سازگار است.
4- از تراکنشهای تو در تو به خوبی پشتیبانی میکنه(Nested Transaction)
using(TransactionScope scope1 = new TransactionScope()) { using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required)) { ... } using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew)) { ... } using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress)) { ... } }
*استفاده از این روش در سیستم هایی که تعداد کاربران آنلاین آن زیاد است و هم چنین تعداد تراکنشهای موجود نیز در سطح سیستم خیلی زیاد باشه مناسب نیست.
*تراکنشهای استفاده شده از این روش کند هستند.(مخصوصا که تراکنش در سطح دیتابیس با تعداد و حجم داده زیاد باشه)
امکان تغییر IsolationLevel در طی انجام یک تراکنش امکان پذیر نیست.
(به شخصه مواد * رو در سطح یک پروژه با شرایط کاربران و حجم داده زیاد تست کردم و نتیجه مطلوب حاصل نشد)
موفق باشید.
Parallel.For & Parallel.ForEach
var nums = Enumerable.Range( 0, 1000000 ).ToArray(); long total = 0; // Use type parameter to make subtotal a long, not an int Parallel.For< long >( 0, nums.Length, () => 0, ( j, loop, subtotal ) => { subtotal += nums[j]; return subtotal; }, x => Interlocked.Add( ref total, x ) ); Console.WriteLine( "The total is {0:N0}", total );
var nums = Enumerable.Range( 0, 1000000 ).ToArray(); long total = 0; Parallel.ForEach< int, long >( nums, // source collection () => 0, // method to initialize the local variable ( j, loop, subtotal ) => // method invoked by the loop on each iteration { subtotal += j; //modify local variable return subtotal; // value to be passed to next iteration }, // Method to be executed when each partition has completed. // finalResult is the final value of subtotal for a particular partition. finalResult => Interlocked.Add( ref total, finalResult ) ); Console.WriteLine( "The total from Parallel.ForEach is {0:N0}", total );
IsInfinity
Console.WriteLine("IsInfinity(3.0 / 0) == {0}.", double.IsInfinity(3.0 / 0) ? "true" : "false");
dynamic Type
با استفاده از نوع dynamic می توان عملیات چک کردن نوع در زمان کامپایل را پشت سر گذاشت و در عوض این عملیات را به زمان اجرا، موکول داد.
نکته: نوع dynamic همانند نوع object در بسیاری از شرایط، یکسان رفتار میکند. اگرچه عملیاتهایی که شامل عبارتهایی از نوع dynamic هستند، یا نوع آن توسط کامپایلر بررسی میشوند و یا پذیرفته نمیشوند. کامپایلر اطلاعات مربوط به یک پردازش (روند) را یکجا بسته بندی میکند و این اطلاعات را بعداً در زمان اجرا ارزیابی میکند. به عنوان بخشی از این پردازش، متغیرهایی از نوع dynamic به متغیرهایی از نوع object کامپایل میشوند. بنابراین نوع dynamic فقط در زمان کامپایل وجود دارند (نه در زمان اجرا).
var i = 20; dynamic dynamicVariable = i; Console.WriteLine( dynamicVariable ); var stringVariable = "Example string."; dynamicVariable = stringVariable; Console.WriteLine( dynamicVariable ); var dateTimeVariable = DateTime.Today; dynamicVariable = dateTimeVariable; Console.WriteLine( dynamicVariable ); // The expression returns true unless dynamicVariable has the value null. if ( dynamicVariable is dynamic ) Console.WriteLine( "dynamicVariable variable is dynamic" ); // dynamic and the as operator. dynamicVariable = i as dynamic; // throw RuntimeBinderException if the associated object doesn't have the specified method. // The code is still compiling successfully. Console.WriteLine( dynamicVariable.ToNow1 );
همانطور که در مثال بالا مشاهده میکنید، شما میتوانید متغیرهایی از نوعهای مختلف را به یک شی از نوع dynamic اختصاص دهید. همچنین میتوانید برای بررسی یک متغیر که از نوع dynamic است یا خیر، از عملگر is استفاده کنید. اگر یک خصوصیت را که وجود ندارد، درخواست کنید (خط آخر مثال بالا)، خطای RuntimeBinderException پرتاب میشود.
ExpandoObject
dynamic sampleObject = new ExpandoObject(); sampleObject.FirstName = "Vahid"; sampleObject.LastName = "Mohammad Taheri"; sampleObject.Age = "28"; sampleObject.TestRemoveProperty = DateTime.Now; sampleObject.AsString = new Action( () => Console.WriteLine( "{0} {1} is {2} years old.", sampleObject.FirstName, sampleObject.LastName, sampleObject.Age ) ); sampleObject.AsString();
( (IDictionary< String, Object >)sampleObject ).Remove( "TestRemoveProperty" );
یک نکتهی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئریهای خود ارجاعی
همانطور که در مطلب «مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجهی آن، بسیار ساده میشود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسیهای ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت میتوان به صورت زیر عمل کرد:
الف) تشکیل یک کلاس پایه برای تعریف سادهتر و مشخص رابطههای خود ارجاعی
public abstract class BaseEntity { public int Id { get; set; } } public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity where TSelfEntity : BaseEntity { public virtual TSelfEntity? Reply { set; get; } public int? ReplyId { get; set; } public virtual ICollection<TSelfEntity>? Children { get; set; } }
که ساختار معرفی شدهی در اینجا، با توضیحات موجود در متن، انطباق دارد.
ب) پر کردن درخت نهایی حاصل به صورت دستی:
چون دیگر EF این درخت را برای ما تشکیل نمیدهد، اکنون باید خودمان کار تشکیل آنرا به صورت زیر انجام دهیم:
public static class SelfReferencingExtensions { public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList) where TEntity : BaseSelfReferencingEntity<TEntity> { var results = new List<TEntity>(); if (originalList is null || originalList.Count == 0) { return results; } foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue)) { results.Add(rootItem); AppendChildren(originalList, rootItem); } return results; } private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem) where TEntity : BaseSelfReferencingEntity<TEntity> { foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id)) { parentItem.Children ??= new List<TEntity>(); parentItem.Children.Add(kid); AppendChildren(originalList, kid); } } }
در اینجا کار تشکیل درخت نهایی، با استفاده از یک متد بازگشتی، انجام میشود.
پس از این مقدمات، نحوهی استفاده از آن به صورت زیر است:
var comments = await _comments.AsNoTracking() .Where(x => x.ParentId == postId) .OrderBy(x => x.Id) .Take(count) .ToListAsync(); var commentsTree = comments.ToSelfReferencingTree();
کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکتهی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده میشوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخها را به صورت خودکار انجام میدهد.
ایجاد یک پروژهی جدید Blazor WASM
برای پیاده سازی و اجرای مثالهای این قسمت، نیاز به یک پروژهی جدید Blazor WASM را داریم که میتوان آنرا با اجرای دستور dotnet new blazorwasm --hosted در یک پوشهی خالی، ایجاد کرد.
یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهدهی لیست آنها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب میشود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.
حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشهی Client ،Server و Shared تشکیل میشود:
در اینجا یک پروژهی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آنرا بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژهی استاندارد ASP.NET Core Web API را در پوشهی جدید Server ایجاد کرده که کار ارائهی اطلاعات این سرویس آب و هوا را انجام میدهد و برنامهی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت میکند.
بنابراین اگر مدل برنامهای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آنرا بسیار ساده میکند.
نکته: روش اجرای این نوع برنامهها با اجرای دستور dotnet run در داخل پوشهی Server پروژه، انجام میشود. با اینکار هم سرور ASP.NET Core آغاز میشود و هم برنامهی WASM توسط آن ارائه میگردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمتهای بدون نیاز به سرور پروژهی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.
شروع به کار با Razor
پس از ایجاد یک پروژهی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیشفرض آنرا بجز سطر اول زیر، حذف میکنیم:
@page "/"
در فایلهای razor. میتوان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/" <p>Hello, @name</p> @code { string name = "Vahid N."; }
یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آنها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.
در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
<p>Hello, @name.ToUpper()</p>
یا حتی میتوان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/" <p>Hello, @name.ToUpper()</p> <p>Hello, @CustomToUpper(name)</p> @code { string name = "Vahid N."; string CustomToUpper(string value) => value.ToUpper(); }
<p>Let's add 2 + 2 : @2 + 2 </p>
<p>Let's add 2 + 2 : @(2 + 2) </p>
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا اگر از Console.WriteLine("Test")@ استفاده میشد، به معنای انتساب یک رشتهی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/" <button @onclick="@WriteLog">Click me 2</button> @code { void WriteLog() { Console.WriteLine("Test"); } }
@page "/" <button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button> @code { void WriteLogWithParam(string value) { Console.WriteLine(value); } }
یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش میدهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments
امکان تعریف کلاسها در فایلهای razor.
در فایلهای razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاسها نیز وجود دارد و همچنین میتوان از کلاسهای خارجی (کلاسهایی که خارج از فایل razor جاری تعریف شدهاند) نیز استفاده کرد.
@page "/" <p>Hello, @StringUtils.MyCustomToUpper(name)</p> @code { public class StringUtils { public static string MyCustomToUpper(string value) => value.ToUpper(); } }
البته این کلاس را تنها میتوان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننتها نیز استفاده کرد، میتوان آنرا در پروژهی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شدهاست:
الف) پروژهی کلاینت: که همان WASM است.
ب) پروژهی سرور: که یک پروژهی ASP.NET Core Web API ارائه کنندهی سرویس و API آب و هوا است و همچنین هاست کنندهی WASM ما.
ج) پروژهی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته میشوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفتهی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژههای سرور و کلاینت تکرار کنیم و میتوان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاسهای کمکی است؛ مانند StringUtils فوق. به همین به پروژهی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف میکنیم (و یا حتی میتوان این قطعه کد را داخل یک پوشهی جدید، در همان پروژهی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared { public class StringUtils { public static string MyNewCustomToUpper(string value) => value.ToUpper(); } }
پس از آن روش استفادهی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/" @using BlazorRazorSample.Shared <p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
یک نکته: میتوان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
کار با حلقهها در فایلهای razor.
همانطور که عنوان شد، یکی از کاربردهای پروژهی Shared، امکان به اشتراک گذاشتن مدلها، در برنامههای کلاینت و سرور است. برای مثال یک پوشهی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف میکنیم:
using System; namespace BlazorRazorSample.Shared.Models { public class MovieDto { public string Title { set; get; } public DateTime ReleaseDate { set; get; } } }
@using BlazorRazorSample.Shared.Models
@page "/" <div> <h3>Movies</h3> @foreach(var movie in movies) { <p>Title: <b>@movie.Title</b></p> <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p> } </div> @code { List<MovieDto> movies = new List<MovieDto> { new MovieDto { Title = "Movie 1", ReleaseDate = DateTime.Now.AddYears(-1) }, new MovieDto { Title = "Movie 2", ReleaseDate = DateTime.Now.AddYears(-2) }, new MovieDto { Title = "Movie 3", ReleaseDate = DateTime.Now.AddYears(-3) } }; }
یک نکته: در حین تعریف فیلدهای code@، امکان استفادهی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنهی یک متد استفاده کنیم.
و یا نمونهی دیگری از حلقههای #C مانند for را میتوان به صورت زیر تعریف کرد:
@for(var i = 0; i < movies.Count; i++) { <div style="background-color: @(i % 2 == 0 ? "blue" : "red")"> <p>Title: <b>@movies[i].Title</b></p> <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p> </div> }
نمایش شرطی عبارات در فایلهای razor.
اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata" @using BlazorRazorSample.Shared @inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }
برای رفع این مشکل، ابتدا یک if@ مشاهده میشود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null) { <p><em>Loading...</em></p> }
روش نمایش عبارات HTML در فایلهای razor.
فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto { Title = "<i>Movie 1</i>", ReleaseDate = DateTime.Now.AddYears(-1) },
<p>Title: <b>@((MarkupString)movie.Title)</b></p>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشهی Server شده و دستور dotnet run را اجرا کنید.
reCAPTCHA چیست؟
استفاده آسان و امنیت بالا، جملهای میباشد که گوگل در سرتیتر تعریف آن جای داده که البته عنوان «من روبات نیستم» در سرویس استفاده شدهاست. reCAPTCHA یک سرویس رایگان برای وب سایتهای شما در جهت حفظ آن در برابر روباتهای مخرب است و از موتور تجزیه و تحلیل پیشرفتهی تشخیص انسان در برابر روباتها استفاده مینماید. reCAPTCHA را میتوان به صورت ماژول در بلاگ و یا فرمهای ثبت نام و ... جای داد که فقط با یک کلیک هویت سنجی انجام خواهد شد. گاها ممکن است بجای کلیک از شما سوالی پرسیده شود که در این صورت میبایستی تصاویر مرتبط با آن سوال را تیک زده باشید. دلیل استفاده از reCAPTCHA:
- گزارش روزانه از وضعیت موفقیت آمیز بودن هویت سنجی
- سهولت استفاده برای کاربران
- سهولت استفاده جهت برنامه نویسان
- دسترسی پذیری مناسب بدلیل وجود سؤالات تصویری و تلفظ و پخش عبارت بصورت صوتی
- امنیت بالا
آیا میتوان قالب reCAPTCHA را تغییر داد؟
جواب این سوال بله میباشد. این سرویس در دو قالب سفید و مشکی ارائه شدهاست که به صورت پیش فرض قالب سفید آن انتخاب میشود. در تصویر زیر قالبهای این سرویس را مشاهده خواهید کرد.
زبانهای پشتیبانی شده در این سرویس:
اضافه نمودن reCAPTCHA به سایت:
اگر قبلا در گوگل ثبت نام نمودهاید کافیست وارد این سایت شوید و بر روی Get reCAPTCHA کلیک نمائید؛ در غیر اینصورت میبایستی یک حساب کاربری ایجاد نماید. بعد از ورود، به کنترل پنل هدایت خواهید شد. در نمای اول به تصویر زیر برخورد خواهید کرد که از شما ثبت سایت جدید را خواستار است:
نام، دامنه سایت و مالک را وارد و ثبت نام نماید.
پس از آنکه بر روی دکمهی ثبت نام کلیک نمودید، برای شما دو کلید جدید را ثبت مینماید که منحصر به سایت شماست. Site Key رشته ای را داراست که در کدهای HTML قرار خواهد گرفت و کلید بعدی Secret Key میباشد. ارتباط سایت شما با گوگل میبایستی به صورت محرمانه محفوظ بماند.
گامهای لازم جهت نمایش سرویس در سایت:
- دستورات سمت کاربر
- دستورات سمت سرور
دستورات سمت کاربر:
کد زیر را در قبل از بسته شدن تک <head/> قرار دهید:
<script src='https://www.google.com/recaptcha/api.js'></script>
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>
نکته: مقدار data-sitekey برابر است با رشته Site Key که گوگل برای شما ثبت نمود.
دستورات سمت سرور:
وقتی کاربر فرم حاوی کپچا را که به صورت صحیح هویت سنجی آن انجام شده باشد به سمت سرور ارسال کند، به عنوان بخشی از دادهی ارسال شده، یک رشته با نام g-recaptcha-response با دستور Request دریافت خواهید کرد که به منظور بررسی اینکه آیا گوگل تایید کرده است که کاربر، یک درخواست POST ارسال نموداست. با این پارامترها یک مقدار json برگشت داده خواهد شد که میبایستی کلاسی متناظر با آن جهت خواندن ساخته شود.
با استفاده از کد زیر مقدار برگشتی Json را در کلاس مپ مینمائیم:using System.Collections.Generic; using Newtonsoft.Json; namespace BaseConfig.Security.Captcha { public class RepaptchaResponse { [JsonProperty("success")] public bool Success { get; set; } [JsonProperty("error-codes")] public List<string> ErrorCodes { get; set; } } }
با استفاده از کلاس زیر درخواستی به گوگل ارسال شده و در صورتیکه با خطا مواجه شود با استفاده از دستور switch به آن دسترسی خواهیم یافت.
using System.Configuration; using System.Net; using Newtonsoft.Json; namespace BaseConfig.Security.Captcha { public class ReCaptcha { public static string _secret; static ReCaptcha() { _secret = ConfigurationManager.AppSettings["ReCaptchaGoogleSecretKey"]; } public static bool IsValid(string response) { //secret that was generated in key value pair var client = new WebClient(); var reply = client.DownloadString($"https://www.google.com/recaptcha/api/siteverify?secret={_secret}&response={response}"); var captchaResponse = JsonConvert.DeserializeObject<RepaptchaResponse>(reply); // when response is false check for the error message if (!captchaResponse.Success) { //if (captchaResponse.ErrorCodes.Count <= 0) return View(); //var error = captchaResponse.ErrorCodes[0].ToLower(); //switch (error) //{ // case ("missing-input-secret"): // ViewBag.Message = "The secret parameter is missing."; // break; // case ("invalid-input-secret"): // ViewBag.Message = "The secret parameter is invalid or malformed."; // break; // case ("missing-input-response"): // ViewBag.Message = "The response parameter is missing."; // break; // case ("invalid-input-response"): // ViewBag.Message = "The response parameter is invalid or malformed."; // break; // default: // ViewBag.Message = "Error occured. Please try again"; // break; //} return false; } // Captcha is valid return true; } } }
تابع IsValid از نوع برگشتی Boolean بوده و خطایی برگشت داده نخواهد شد و از این جهت به صورت کامنت برای شما گذاشته شده که میتوان متناظر با کد نویسی آن را تغییر دهید.
در اکشن زیر مقدار response برسی میشود تا خالی نباشد و همچنین مقدار آن را میتوان با استفاده از تابع IsValid در کلاس ReCaptcha به سمت گوگل فرستاد.
// // POST: /Account/Login [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> Login(LoginPageModel model, string returnUrl) { var response = Request["g-recaptcha-response"]; if (response != null && ReCaptcha.IsValid(response)) { // } }
گاها اتفاق میافتد که از دستورات Ajax برای ارسال اطلاعات به سمت سرور استفاده میشود که در این صورت لازم است بعد از پایان عملیات، کپچا ریفرش گردد. برای این کار میتوان از دستور جاوا اسکریپتی زیر استفاده نمود. در صورتیکه صفحه Postback شود، کپچا مجددا ریفرش خواهد شد.
/** * * @param {} data * @returns {} */ function Success(data) { grecaptcha.reset(); }
تا اینجا موفق شدیم تا فرم ارسالی همراه کپچا را به سمت سرور ارسال کنیم. اما ممکن است در یک صفحه از چند کپچا استفاده شود که در این صورت میبایستی دستورات سمت کاربر تغییر نمایند.
برای این کار دستور
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>
<script> var recaptcha1; var recaptcha2; var myCallBack = function () { //Render the recaptcha1 on the element with ID "recaptcha1" recaptcha1 = grecaptcha.render('recaptcha1', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); //Render the recaptcha2 on the element with ID "recaptcha2" recaptcha2 = grecaptcha.render('recaptcha2', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); //Render the recaptcha3 on the element with ID "recaptcha3" recaptcha2 = grecaptcha.render('recaptcha3', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); }; </script>
برای نمایش کپچا، تگهای div با id متناظر با recaptcha1, recaptcha2, recaptcha3 ( در این مثال از سه کپچا در صفحه استفاده شده است ) در صفحه قرار خواهند گرفت.
<div id="recaptcha1"></div> <div id="recaptcha2"></div> <div id="recaptcha3"></div>
کار ما تمام شد. حال اگر پروژه را اجرا نمائید، در صفحه سه کپچا مشاهده خواهید کرد.
چند زبانه کردن کپچا:
برای چند زبانه کردن کافیست با مراجعه به این لینک و یا استفاده از تصویر بالا ( زبانهای پشتیبانی ) مقدار آن زبان را برابر با پراپرتی hl که به صورت کوئری استرینگ برای گوگل ارسال میگردد، استفاده نمود. کد زیر نمونهی استفاده شده برای زبانهای انگلیسی و فارسی میباشد که با ریسورس مقدار دهی میشود.<script src='https://www.google.com/recaptcha/api.js?hl=@(App_GlobalResources.CP.CurrentAbbrivation)'></script>
در صورتی که از فایل ریسوس استفاده نمیکنید میتوان به صورت مستقیم مقدار دهی نمائید:
<script src='https://www.google.com/recaptcha/api.js?hl=fa'></script>
برای دوستانی که با تکنولوژی ASP.Net کار میکنند، این روال هم برای آنها هم صادق میباشد.
برای دریافت پروژه اینجا کلیک نمائید.