مطالب
یکپارچه کردن ELMAH با WCF RIA Services

پیشتر در مورد ELMAH مطلبی را منتشر کرده بودم و اگر برنامه نویس ASP.NET هستید و با ELMAH آشنایی ندارید،‌ جدا نیمی از عمر کاری شما بر فنا است!
هاست پیش فرض یک WCF RIA Service هم یک برنامه‌ی ASP.NET است. بنابراین کلیه‌ی خطاهای رخ داده در سمت سرور را باید بتوان به نحوی لاگ کرد تا بعدا با مطالعه‌ی آن‌ها اطلاعات ارزشمندی را از نقایص برنامه در عمل و پیش از گوشزد شدن آن‌ها توسط کاربران، دریافت، بررسی و رفع کرد.
کلیه خطاها را لاگ می‌کنم تا:
- بدانم معنای جمله‌ی "برنامه کار نمی‌کنه" چی هست.
- بدون روبرو شدن با کاربران یا حتی سؤال و جوابی از آن‌ها بدانم دقیقا مشکل از کجا ناشی شده.
- بدانم رفتارهای عمومی کاربران که منجر به بروز خطا می‌شوند کدام‌ها هستند.
- بدانم در کدامیک از قسمت‌های برنامه تعیین اعتبار ورودی کاربران یا انجام نشده یا ضعیف و ناکافی است.
- بدانم زمانیکه دوستی (!) قصد پایین آوردن برنامه را با تزریق SQL داشته، دقیقا چه چیزی را وارد کرده، در کجا و چه زمانی؟
- بتوانم Remote worker خوبی باشم.

ELMAH هم برای لاگ کردن خطاهای مدیریت نشده‌ی یک برنامه‌ی ASP.NET ایجاد شده است. بنابراین باید بتوان این دو (WCF RIA Services و ELMAH) را به نحوی با هم سازگار کرد. برای اینکار نیاز است تا یک مدیریت کننده‌ی خطای سفارشی را با پیاده سازی اینترفیس IErrorHandler تهیه کنیم (تا خطاهای مدیریت نشده‌ی حاصل را به سمت ELMAH هدایت کند) و سپس آن‌را به کمک یک ویژگی یا Attribute به DomainService خود جهت لاگ کردن خطاها اعمال نمائیم. روش تعریف این Attribute را در کدهای بعد ملاحظه خواهید نمود (در اینجا نیاز است تا دو ارجاع را به اسمبلی‌های Elmah.dll که دریافت کرده‌اید و اسمبلی استاندارد System.ServiceModel نیز به پروژه اضافه نمائید):

//add a reference to "Elmah.dll"
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Web;

namespace ElmahWcf
{
public class HttpErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
return false;
}

public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error == null)
return;

if (HttpContext.Current == null) //In case we run outside of IIS
return;

Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
#endregion
}
}

//add a ref to "System.ServiceModel" assembly
using System;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace ElmahWcf
{
public class ServiceErrorBehaviorAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviorAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}

#region IServiceBehavior Members

public void AddBindingParameters(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{ }

public void ApplyDispatchBehavior(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
cd.ErrorHandlers.Add(errorHandler);
}
}

public void Validate(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{ }
#endregion
}
}
اکنون پس از تعریف ویژگی ServiceErrorBehavior، نوبت به اعمال آن می‌رسد. به فایل DomainService خود مراجعه کرده و یک سطر زیر را به آن اضافه نمائید:
    [ServiceErrorBehavior(typeof(HttpErrorHandler))] //Integrating with ELMAH
[EnableClientAccess()]
public partial class MyDomainService : LinqToEntitiesDomainService<myEntities>

در ادامه نحوه‌ی افزودن تعاریف متناظر با ELMAH به Web.Config برنامه ذکر شده است. این تعاریف برای IIS6 و 7 به بعد هم تکمیل گردیده است. خطاها هم به صورت فایل‌های XML در پوشه‌ای به نام Errors که به ریشه‌ی سایت اضافه خواهید نمود (یا هر پوشه‌ی دلخواه دیگری)، لاگ می‌شوند.
به نظر من این روش، از ذخیره سازی اطلاعات لاگ‌ها در دیتابیس بهتر است. چون اساسا زمانیکه خطایی رخ می‌دهد شاید مشکل اصلی همان ارتباط با دیتابیس باشد.
قسمت ارسال خطاها به صورت ایمیل نیز comment شده است که در صورت نیاز می‌توان آن‌را فعال نمود:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

<elmah>
<security allowRemoteAccess="1" />
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Errors" />
<!-- <errorMail
from="errors@site.net"
to="nasiri@site.net"
subject="prj-error"
async="true"
smtpPort="25"
smtpServer="mail.site.net"
noYsod="true" /> -->
</elmah>

<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</modules>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</handlers>
</system.webServer>
<system.web>
<globalization
requestEncoding="utf-8"
responseEncoding="utf-8"
/>
<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403AuthV"
cookieless="UseCookies"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200" />
</authentication>
<httpHandlers>
<add verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</assemblies>
</compilation>
</system.web>
<connectionStrings>
</connectionStrings>
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
</configuration>
اکنون برای مثال به یکی از متدهای DomainService خود سطر زیر را اضافه کرده و برنامه را آزمایش کنید:
throw new Exception("This is an ELMAH test");

سپس به آدرس http://localhost/myelmah.axd مراجعه نموده و اطلاعات لاگ شده حاصل را بررسی کنید:


این روش با WCF Services های متداول هم کار می‌کند. فقط در این سرویس‌ها باید aspNetCompatibilityEnabled مطابق تگ‌های ذکر شده‌ی system.serviceModel فوق در web.config لحاظ شوند (این مورد به صورت پیش فرض در WCF RIA Services وجود دارد). همچنین ویژگی زیر نیز باید به سرویس شما اضافه گردد:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

منابع مورد استفاده:
Integrating ELMAH for a WCF Service
Making WCF and ELMAH play nice together
Getting ELMAH to work with WCF services



پ.ن.
اگر به خطاهای ASP.NET دقت کرده باشید که به yellow screen of death هم مشهور هستند (در مقابل صفحات آبی ویندوز!)، ابتدای آن خیلی بزرگ نوشته شده Server Error و سپس ادامه‌ی خطا. همین مورد دقیقا یادم هست که هر بار سبب بازخواست مدیران شبکه بجای برنامه نویس‌ها می‌شد! (احتمالا این هم یک نوع بدجنسی تیم ASP.NET برای گرفتن حال ادمین‌های شبکه است! و گرنه مثلا می‌توانستند همان ابتدا بنویسند program/application error بجای server error)

مطالب دوره‌ها
بررسی حالت‌های مختلف نصب RavenDB
چهار روش مختلف برای نصب، استفاده و توزیع RavenDB وجود دارند. ساده‌ترین روش آن‌را که اجرای فایل Raven.Server.exe است، تاکنون بررسی کردیم. این روش صرفا جهت دیباگ و کار برنامه نویسی مناسب است. در ادامه سه روش دیگر را بررسی خواهیم کرد.

الف) استفاده از RavenDB در حالت مدفون شده یا Embedded

حالت Embedded به این معنا است که RavenDB درون پروسه برنامه شما اجرا خواهد شد و نه به صورت پروسه‌ای مجزا. این حالت برای ارائه ساده برنامه‌های دسکتاپ بسیار مناسب است؛ یا حتی توزیع برنامه‌های سبک ASP.NET بدون نیاز به نصب بانک اطلاعاتی خاصی بر روی وب سرور.
برای کار با RavenDB در حالت Embedded ابتدا فایل‌های مورد نیاز آن‌را از طریق نیوگت دریافت کنید:
 PM> Install-Package RavenDB.Embedded -Pre
در این حالت فایل کلاینت مورد نیاز، اسمبلی Raven.Client.Embedded.dll خواهد بود. سپس در کدهای قبلی خود بجای استفاده از new DocumentStore، اینبار خواهیم داشت new EmbeddableDocumentStore.
 var documentStore = new EmbeddableDocumentStore { DataDirectory = @"~/app_data/ravendb" };
documentStore.Initialize();
سایر قسمت‌های برنامه نیازی به تغییر نخواهند داشت.
امکان تعریف DataDirectory در فایل کانفیگ برنامه نیز وجود دارد. فقط در این حالت باید دقت داشت که نام مسیر، با DataDir شروع می‌شود و نه DataDirectory :
<connectionStrings>
  <add name="Local" connectionString="DataDir = ~\Data"/>
سپس همانند قبل، مقدار خاصیت رشته اتصالی EmbeddableDocumentStore به نام مدخل فوق باید تنظیم گردد.


چند نکته جالب در مورد حالت Embedded
- امکان اجرای درون حافظه‌ای RavenDB نیز وجود دارد:
 var documentStore = new EmbeddableDocumentStore{RunInMemory = true}.Initialize()
در اینجا فقط کافی است خاصیت RunInMemory شیء EmbeddableDocumentStore به true تنظیم شود. این مورد بسیار مناسب است برای انجام آزمون‌های واحد بسیار سریع که پس از پایان کار برنامه، اثری از بانک اطلاعاتی آن باقی نخواهد ماند.
- اجرای حالت Embedded به صورت Embedded HTTP:
در حالت Embedded دیگر دسترسی به برنامه سیلورلایت Raven studio وجود ندارد. اگر علاقمند به کار با آن بودید، خاصیت UseEmbeddedHttpServer شیء EmbeddableDocumentStore را به true تنظیم کنید. سپس فایل Raven.Studio.xap را در ریشه وب سایت خود قرار دهید. اکنون مانند قبل آدرس localhost:8080/raven/studio.html برقرار خواهد بود.
همچنین سرور Http این بانک اطلاعاتی را نیز می‌توان دستی راه اندازی کرد. متد NonAdminHttp.EnsureCanListenToWhenInNonAdminContext بررسی می‌کند که آیا برنامه مجوز راه اندازی یک سرور را بر روی پورت مثلا 8080 دارد یا خیر.
 NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(8080);

// Start the HTTP server manually
var server = new RavenDbHttpServer(documentStore.Configuration, documentStore.DocumentDatabase);
server.Start();


ب) نصب RavenDB به صورت سرویس ویندوز NT
اگر مایل باشیم تا RavenDB را نیز مانند SQL Server به صورت یک سرویس ویندوز NT نصب کنیم تا همواره در پس زمینه سرور در حال اجرا باشد، کنسول پاورشل ویندوز را گشوده و سپس فرمان ذیل را صادر کنید:
 d:\ravendb\server> .\raven.server.exe /install
اکنون اگر به کنسول مدیریتی سرویس‌های ویندوز یا services.msc مراجعه کنید، ravendb را به صورت یک آیتم جدید در لیست سرویس‌های ویندوز خواهید یافت.
و اگر خواستید این سرویس را عزل کنید، دستور ذیل را در پاورشل ویندوز صادر کنید:
 d:\ravendb\server> .\raven.server.exe /uninstall


ج) نصب RavenDB به صورت یک پروسه IIS (یا اجرا شده توسط IIS)

فایل‌های مورد نیاز حالت اجرای RavenDB را به صورت یک پروسه مجزای IIS از نیوگت دریافت کنید:
 PM> Install-Package RavenDB.AspNetHost -Pre
در این حالت، پوشه bin، فایل xap و فایل کانفیگ برنامه وب مورد نیاز دریافت خواهند شد. پس از آن، تنها کافی است یک دایرکتوری مجازی را در IIS به این پوشه جدید اختصاص داده و همچنین بهتر است یک Application pool جدید را نیز برای آن تهیه کنید تا واقعا این برنامه در پروسه‌ی مجزای خاص خودش اجرا شود. حتی در این حالت می‌توان شماره پورت دیگری را به این برنامه اختصاص داد. به علاوه در این حالت تنظیمات Recycling مربوط به IIS را هم باید مدنظر داشت (در قسمت تنظیمات Application pool برنامه) و مثلا تنظیم کرد که برنامه پس از چه مدت فعال نبودن از حافظه خارج شود.


یک نکته
تمام بسته‌های مورد نیاز را یکجا از آدرس http://ravendb.net/download نیز می‌توان دریافت کرد. در نگارش‌های جدید، بسته نصاب نیز برای این بانک اطلاعاتی تهیه شده است که برای نمونه توزیع آن‌را جهت حالت نصب در IIS ساده‌تر می‌کند.
نظرات مطالب
مروری بر کاربردهای Action و Func - قسمت اول
طراحی IComparable مربوط به زمان دات نت یک است. اگر آن زمان امکانات زبان مثل امروز بود، می‌شد از طراحی ساده‌تری استفاده کرد.
یک نمونه از طراحی‌های اخیر تیم دات نت رو میشه در WebGrid دید. در این طراحی برای نمونه جهت دریافت فرمول فرمت کردن مقدار یک cell، از Func استفاده کردن. می‌شد این رو با اینترفیس هم نوشت (چون قرار است کاری به خارج از کلاس محول شود و هر بار اطلاعاتی به آن ارسال و نتیجه‌ای جدید اخذ گردد؛ پیاده سازی آن با شما، نتیجه را فقط در اختیار WebGrid ما قرار دهید). اما جدا استفاده از آن تبدیل می‌شد به عذاب برای کاربر که به نحو زیبایی با Func و امکانات جدید زبان حل شده.
مطالب
MSBuild
MSBuild
به عنوان یک تعریف کلی، مایکروسافت بیلد (Microsoft Build)، پلتفرمی برای ساخت اپلیکیشن‌هاست. در این پلتفرم (که با عنوان MSBuild شناخته میشود) کلیه تنظیمات لازم برای تولید و ساخت یک اپلیکیشن درون یک فایل XML ذخیره میشود، که به آن فایل پروژه میگویند. ویژوال استودیو نیز از این ابزار برای تولید تمامی اپلیکیشن‌ها استفاده می‌کند، اما MSBuild به ویژوال استودیو وابسته نیست و کاملا مستقل از آن است.
این ابزار به همراه دات نت فریمورک (البته نسخه کامل آن و نه نسخه‌های سبکتری چون Client Profile) نصب میشود. بنابراین با استفاه از فایل اجرایی این ابزار (msbuild.exe) میتوان فرایند بیلد را برای پروژه و یا سولوشن‌های خود، بدون نیاز به نصب ویژوال استودیو اجرا کرد. استفاده مستقیم از MSBuild در شرایط زیر نیاز میشود:
- ویزوال استودیو در دسترس نباشد.
- نسخه 64 بیتی این ابزار که در ویژوال استودیو در دسترس نیست. البته در بیشتر مواقع این مورد پیش نخواهد آمد مگر اینکه برای فرایند بیلد به حافظه بیشتری نیاز باشد.
- اجرای فرایند بیلد در بیش از یک پراسس (برای رسیدن به سرعت بالاتر). این امکان در تولید پروژه‌های ++C در ویژوال استودیو موجود است. همچنین از نسخه 2012 این امکان برای پروژه‌های #C نیز فراهم شده است.
- سفارشی‌سازی فرایند بیلد
- و ...
همچنین یکی دیگر از بخشهای مهم فرایندِ تولیدِ اپلیکیشن که همانند ویژوال استودیو از این ابزار بصورت مستقیم استفاده میکند Team Foundation Build است.
با استفاده از خط فرمان این ابزار تنظیمات فراوانی را برای سفارشی سازی عملیات بیلد میتوان انجام داد که شرح آنها بحثی مفصل میطلبد. تنظیمات بسیار دیگری هم در فایل پروژه قابل اعمال است (توضیحات بیشتر در اینجا). منابع برای مطالعه بیشتر:
 Microsoft Build API
در دات‌نت فریمورک فضای نامی با عنوان Microsoft.Build نیز وجود دارد که امکانات این ابزار را در اختیار برنامه نویس قرار میدهد. برای استفاده از این کتابخانه باید ارجاعی به اسمبلی آن داد، که به همین نام بوده و به همراه دات‌نت فریمورک نصب میشود. کد زیر نحوه استفاده اولیه از این کتابخانه را نشان میدهد:
private static void TestMSBuild(string projectFullPath)
{
  var pc = new ProjectCollection();
  var globalProperties = new Dictionary<string, string>() { { "Configuration", "Debug" }, { "Platform", "AnyCPU" } };
  var buidlRequest = new BuildRequestData(projectFullPath, globalProperties, null, new string[] { "Build" }, null);
  var buildResult = BuildManager.DefaultBuildManager.Build(new BuildParameters(pc), buidlRequest);
}
با اینکه ارائه مقداری غیرنال برای آرگومان globalProperties اجباری است اما پرکردن آن کاملا اختیاری است، زیرا تمام تنظیمات ممکن را میتوان در خود فایل پروژه ثبت کرد.
برای مطالعه بیشتر منابع زیر پیشنهاد میشود:
استفاده از msbuild.exe
ابزار msbuild به صورت یک فایل exe در دسترس است و برای استفاده از آن میتوان از خط فرمان ویندوز استفاده کرد. مسیر فایل اجرایی آن (MSBuild.exe) در ریشه مسیر دات نت فریمورک است، بصورت زیر:
نسخه 32 بیتی:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe
نسخه 64 بیتی:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe
برای استفاده از آن میتوان مسیر فایل پروژه یا سولوشن (فایل با پسوند csprj. یا vbprj. یا sln.) را به آن داد تا سایر عملیات تولید را به صورت خودکار تا آخر به انجام برساند. کاری که عینا در ویژوال استودیو در زمان Build انجام میشود! برای بهره برداری از آن در کد میتوان از کلاس Process استفاده کرد. برای مسیر این فایل هم میتوان از نشانی‌هایی که در بالا معرفی شد استفاده کرد یا برای راحتی و امنیت بیشتر از کلید رجیستری مربوطه که در کد زیر نشان داده شده استفاده کرد:
private static void TestMSBuild1(string projectPath)
{
  var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0");
  if (regKey == null) return;
  var msBuildExeFilePath = Path.Combine(regKey.GetValue("MSBuildToolsPath").ToString(), "MSBuild.exe");
  var startInfo = new ProcessStartInfo
                    {
                      FileName = msBuildExeFilePath,
                      Arguments = projectPath,
                      WindowStyle = ProcessWindowStyle.Hidden
                    };
  var process = Process.Start(startInfo);
  process.WaitForExit();
}
بدین ترتیب عملیاتی مشابه عملیات Build در ویژوال استودیو انجام میشود و با توجه به تنظیمات موجود در فایل پروژه، پوشه‌های خروجی (مثلا bin و obj در حالت پیش فرض پروژه‌های ویژوال استودیو) نیز در مسیرهای مربوطه ایجاد میگردد.
مطالب
تبدیل HTML به PDF با استفاده از کتابخانه‌ی iTextSharp

روش متداول کار با کتابخانه‌ی iTextSharp ، ایجاد شیء Document ، سپس ایجاد PdfWriter برای نوشتن در آن، گشودن سند و ... افزودن اشیایی مانند Paragraph ، PdfPTable ، PdfPCell و غیره به آن است و در نهایت بستن سند. راه میانبری هم برای کار با این کتابخانه وجود دارد و آن هم استفاده از امکانات فضای نام iTextSharp.text.html.simpleparser آن می‌باشد. به این ترتیب می‌توان به صورت خودکار، یک محتوای HTML را تبدیل به فایل PDF کرد.

مثال : نمایش یک متن HTML ساده انگلیسی
using System.Diagnostics;

using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;

namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var html = @"<span style='color:blue'><b>Testing</b></span>
<i>iTextSharp's</i> <u>HTML to PDF capabilities</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), null);

foreach (var htmlElement in parsedHtmlElements)
{
pdfDoc.Add(htmlElement);
}
}

//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}


نکته‌ی جدید کد فوق، استفاده از متد HTMLWorker.ParseToList است. به این ترتیب parser کتابخانه‌ی iTextSharp وارد عمل شده و html تعریف شده را به معادل المان‌های بومی خودش تبدیل می‌کند؛ مثلا تبدیل به chunk یا pdfptable و امثال آن. در نهایت در طی یک حلقه، این عناصر به صفحه اضافه می‌شوند.
البته باید دقت داشت که HTMLWorker امکان تبدیل عناصر پیچیده، تودرتو و چندلایه HTML را ندارد؛ اما بهتر از هیچی است!

همه‌ی این‌ها خوب! اما به درد ما فارسی زبان‌ها نمی‌خورد. همین متغیر html فوق را با یک متن فارسی جایگزین کنید، چیزی نمایش داده نخواهد شد. البته این هم نکته دارد که در ادامه ذکر خواهد شد.
جهت نمایش متون فارسی نیاز است تا نکات ذکر شده در مطلب «فارسی نویسی و iTextSharp» رعایت شوند که شامل:
- تعیین صریح قلم
- تعیین encoding
- استفاده از عناصر دربرگیرنده‌ای است که خاصیت RunDirection را پشتیبانی می‌کنند؛ مانند PdfPCell و غیره


به این ترتیب خواهیم داشت:
using System.Diagnostics;

using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;
using iTextSharp.text.html;

namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

//روش صحیح تعریف فونت
FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");

StyleSheet styles = new StyleSheet();
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.FONTFAMILY, "tahoma");
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, "Identity-H");

var html = @"<span style='color:blue'><b>آزمایش</b></span>
کتابخانه <i>iTextSharp</i> <u>جهت بررسی فارسی نویسی</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), styles);

PdfPCell pdfCell = new PdfPCell { Border = 0 };
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

foreach (var htmlElement in parsedHtmlElements)
{
pdfCell.AddElement(htmlElement);
}

var table1 = new PdfPTable(1);
table1.AddCell(pdfCell);
pdfDoc.Add(table1);
}

//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}

همانطور که ملاحظه می‌کنید ابتدا قلمی در cache قلم‌های این کتابخانه ثبت می‌شود (FontFactory.Register). سپس نوع قلم و encoding آن توسط یک StyleSheet تعریف شده و به HTMLWorker.ParseToList ارسال می‌گردد و در نهایت به کمک یک المان دارای RunDirection، در صفحه نمایش داده می‌شود.



نکته:
ممکن است که به متغیر html ، یک table ساده html را نسبت دهید. در این حالت پس از تنظیم style یاد شده، در هر سلول این html table ، متون فارسی به صورت معکوس نمایش داده خواهند شد که این هم یک نکته‌ی کوچک دیگر دارد:

foreach (var htmlElement in parsedHtmlElements)

{
if (htmlElement is PdfPTable)
{
var table = (PdfPTable)htmlElement;
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
foreach (var row in table.Rows)
{
foreach (var cell in row.GetCells())
{
cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
}
}
}

pdfCell.AddElement(htmlElement);
}

در قسمتی که قرار است المان‌های معادل به pdfCell اضافه شوند، آن‌ها را بررسی کرده و RunDirection آن‌ها را RTL خواهیم کرد.


کاربردها:
بدیهی است این حالت برای تهیه گزارشات پیشرفته‌تر برای مثال تهیه قالب‌هایی که در حین تهیه PDF ، قسمت‌هایی از آن‌ها توسط برنامه نویس Replace می‌شوند، بسیار مناسب است.
همچنین مطلب «بارگذاری یک یوزرکنترل با استفاده از جی‌کوئری» و متد RenderUserControl مطرح شده در آن که در نهایت یک قطعه کد HTML را به صورت رشته به ما تحویل می‌دهد، می‌تواند جهت تهیه گزارش‌های پویایی که برای مثال قسمتی از آن یک GridView بایند شده حاصل از یک یوزر کنترل است،‌ مورد استفاده قرار گیرد.


مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
در ادامه‌ی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آن‌را بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایه‌ای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعه‌ی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایل‌های کانفیگ جدید، تزریق وابستگی‌ها و سرویس‌ها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهنده‌ها و ... در ASP.NET Core 1.0 استفاده خواهد شد.


EF Core چیست؟

EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانک‌های اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر می‌کند. توسط آن قسمت عمده‌ی کدهای مستقیم کار با بانک‌های اطلاعاتی حذف شده و تبدیل به کدهای دات نتی می‌شوند. مزیت این لایه‌ی Abstraction اضافی (لایه‌ای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامه‌است؛ بدون نیاز به تغییری در سایر قسمت‌های برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر می‌کنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر می‌رسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئری‌ها، رفتار بسیاری از بانک‌های اطلاعاتی با آن‌ها همانند رویه‌های ذخیره شده است که به عدم تولید Query plan‌های مجزایی به ازای هر کوئری رسیده منجر می‌شود که در نهایت سبب بالا رفتن سرعت اجرای کوئری‌ها و مصرف حافظه‌ی کمتری در سمت سرور بانک اطلاعاتی می‌گردد.


تفاوت EF Core با نگارش‌های دیگر Entity framework در چیست؟

سورس باز بودن
EF از نگارش‌های آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارش‌های پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.

چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفاده‌ی از آن در انواع و اقسام برنامه‌های دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامه‌های مختلف به شرح زیر است:
 • All .NET application (Console, ASP.NET 4, WinForms, WPF)
 • Mac and Linux applications (Mono)
 • UWP (Universal Windows Platform)
 • ASP.NET Core applications
 • Can use EF Core in Windows phone and Windows store app

افزایش تعداد بانک‌های اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانک‌های اطلاعاتی رابطه‌‌ای بود و همچنین مایکروسافت صرفا نگارش‌های مختلف SQL Server را به صورت رسمی پشتیبانی می‌کرد و برای سایر بانک‌های اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانک‌های اطلاعاتی رابطه‌ای، پشتیبانی از بانک‌های اطلاعاتی NoSQL هم اضافه شده‌است؛ به همراه پروایدر ویژه‌‌ای به نام In Memory جهت انجام ساده‌تر Unit testing. کاری که با نگارش‌های پیشین EF به سادگی و از روز اول پشتیبانی نمی‌شد.

حذف و یا عدم پیاده سازی تعدادی از قابلیت‌های EF 6.x
اگر موارد فوق جزو مهم‌ترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگی‌ها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامه‌های UWP نیاز دارد تمام نوع‌ها را همانند زبان‌های C و ++C، در زمان کامپایل بداند)، یک سری از قابلیت‌های EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمی‌شوند. لیست کامل و به روز شده‌ی این موارد را در اینجا می‌توانید مطالعه کنید.
بنابراین امکان انتقال برنامه‌های EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.


برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0

برای نصب تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بسته‌ی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
البته در این قسمت صرفا از بسته‌ی اول که جهت اتصال به SQL Server است استفاده می‌کنیم. بسته‌های دیگر را در قسمت‌های بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
        "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"
    }
}
این مداخلی که توسط نیوگت اضافه شده‌اند، نیاز به اصلاح دارند؛ به این صورت:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        },
        "Microsoft.EntityFrameworkCore.SqlServer.Design": {
            "version": "1.0.0",
            "type": "build"
        }
    },

    "tools": {
       // same as before
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }   
   }
}
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعه‌ی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آن‌را ذخیره کنید؛ تا کار به روز رسانی و نصب بسته‌ها، به صورت خودکار و همچنین صحیحی انجام شود.


فعال سازی صفحات مخصوص توسعه دهنده‌های EF Core 1.0

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بسته‌ی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحه‌ی نمایش خطاهای بانک اطلاعاتی را میسر می‌کند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{
    "dependencies": {
       // same as before
        "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0"
    }
}
سپس می‌توان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   if (env.IsDevelopment())
   {
      app.UseDatabaseErrorPage();
   }
با فعال سازی این صفحه، اگر در حین توسعه‌ی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهده‌ی یک صفحه‌ی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیه‌هایی از طرف تیم EF مشاهده خواهید کرد.


تعریف اولین Context برنامه و مشخص سازی رشته‌ی اتصالی آن


در این تصویر، زیر ساخت نگاشت‌های EF Core را مشاهده می‌کنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرنده‌ی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل این‌ها را مشاهده می‌کنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که این‌ها نیز به فیلدها و ستون‌های آن جدول در سمت بانک اطلاعاتی نگاشت می‌شوند.
بنابراین برای شروع کار، پوشه‌ای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه می‌کنیم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آن‌را به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق می‌شود، صورت خواهد گرفت:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق می‌شود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار می‌دهیم.
سازنده‌ی این کلاس نیز به نحو خاصی تعریف شده‌است. اگر به سورس‌های EF Core مراجعه کنیم، کلاس پایه‌ی DbContext دارای دو سازنده‌ی با و بدون پارامتر است:
protected DbContext()
   : this((DbContextOptions) new DbContextOptions<DbContext>())
{
}

public DbContext([NotNull] DbContextOptions options)
{
  // …
}
اگر از سازنده‌ی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازنده‌ی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"... connection string ...");
        }
    }
}
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشته‌ی اتصالی به آن چیست.
اما چون در یک برنامه‌ی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام می‌شود و در آنجا به سادگی می‌توان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشته‌ی اتصالی را دریافت کرد، مرسوم است از سازنده‌ی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعه‌ی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» به فایل appsettings.json مراجعه کرده و تنظیمات رشته‌ی اتصالی برنامه را به صورت ذیل در آن مشخص می‌کنیم:
{
    "ConnectionStrings": {
        "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true"
    }
}
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهله‌ی پیش فرض SQL Server اشاره شده‌است؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده می‌شود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیده‌است.

پس از تعریف رشته‌ی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازنده‌ی با پارامتر آن استفاده می‌کنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویس‌های ASP.NET Core معرفی می‌کنیم:
    public class Startup
    {
        public IConfigurationRoot Configuration { set; get; }

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]);
            });
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» نحوه‌ی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفته‌است، نیز ذکر شده‌است.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی می‌کنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.


یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد

با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگی‌ها نیز در آن وجود دارد. یعنی بجای روش فوق، می‌توان IConfigurationRoot را به سازنده‌ی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        private readonly IConfigurationRoot _configuration;

        public ApplicationDbContext(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        //{
        //}

        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]);
        }
    }
}
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعه‌ی سرویس‌های برنامه معرفی شده‌است، از آن در تمام کلاس‌های برنامه که تحت نظر سیستم تزریق وابستگی‌های توکار ASP.NET Core هستند، می‌توان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا می‌کند و ساده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>();


یک نکته: امکان تزریق ApplicationDbContext به تمام کلاس‌های برنامه وجود دارد

همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامه‌ی ASP.NET Core نیز وجود دارد:
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Core1RtmEmptyTest.Controllers
{
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
}
در اینجا نحوه‌ی تزریق DB Context برنامه را به یک کنترلر مشاهده می‌کنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگی‌ها نام ندارد و هنوز این کنترلر دقیقا وابسته‌است به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.

در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:


علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشده‌است و همچنین ساختار جداول را به آن منتقل نکرده‌ایم که این موارد را در قسمت‌های بعدی مرور خواهیم کرد.
مطالب
استخراج داده های وب سرویس توسط SSIS
یکی از راه‌های انتقال اطلاعات بین زیر سیستم ها، استفاده از وب سرویس‌ها است . در این پست نحوه استخراج اطلاعات از وب سرویس و تبدیل آنها به یک فایل XML را به کمک package‌های SSIS توضیح می‌دهم . 
در قدم اول نیاز به یک سرویس داریم که برای نمونه سرویس زیر را طراحی و پیاده سازی می‌کنیم :
متدی به نام HelloWorld در کلاس GetUserInfo که خروجی آن آرایه ای از کلاس UserInfo است. 


و اجرای سرویس
 


و ساختار سرویس که SSIS به این ساختار نیاز دارد .

 

حال از محیط BIDS یک پروژه SSIS ایجاد می‌کنیم (برای آشنایی با BIDS به این پست مراجعه کنید) و یک Web Service Task به آن اضافه می‌کنیم :

 


سپس روی task دوبار کلیک کنید تا پنجره تنظیمات آن باز شود :

 


حال باید یک دیتا سورس کانکشن که همان سرویس ما می‌باشد تعریف کنیم :


دکمه تست را فشار دهید تا تاییدیه معتبر بودن سرویس را دریافت کنید ، OK را فشار دهید .

در قدم بعد SSIS به قالب فایل xml که در خروجی WSDL است نیاز دارد . اگر این فایل را دارید آدرس آن را باید در قسمت WSDL FIle وارد کنید در غیر این صورت (مانند شرایط فعلی این مثال) باید این فایل را از سرویس خود دانلود کنید . نکته اینکه در صورتی که این فایل را ندارید ، گزینه overwriteWSDLFile را True کنید و مسیر یک فایلی که در سیستم شما موجود نیست را در آدرس WSDLFile وارد کنید . 



حال شما فایل مورد نظر را دارید :

 

اگر به تب input بازگردید می‌توانید ادامه تنظیمات را انجام دهید :

 


حال باید خروجی مورد نظر از این سرویس را تعریف کنیم . دو نوع خروجی برای ما امکان پذیر است . یکی انتقال اطلاعات به یک فایل (مناسب برای مواردی که نیاز به داده‌های offline دارید) و یکی منتقل کردن آنها به متغیر‌های خود SSIS Package برای استفاده در کام‌های بعدی flow (برای مواردی که نیاز به انجام تغییرات روی داده‌های Online دارید) .

 

پس از انجام این تنظیمات باید کانکشن هایی مطابق زیر داشته باشید :

 

F5 را فشار دهید تا عملیات شروع شود :

 و خروجی :

مطالب
Blazor 5x - قسمت 15 - کار با فرم‌ها - بخش 3 - ویرایش اطلاعات
در قسمت قبل، ویژگی‌های ثبت اطلاعات یک اتاق جدید و سپس نمایش لیست آن‌ها را تکمیل کردیم. در این قسمت می‌خواهیم امکان ویرایش آن‌ها را نیز اضافه کنیم.


افزودن دکمه‌ی ویرایش، به رکوردهای لیست اتاق‌ها و نمایش جزئیات رکورد در حال ویرایش

تا اینجا کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor دارای یک چنین مسیریابی است:
@page "/hotel-room/create"
در ادامه می‌خواهیم اگر کاربری برای مثال به آدرس hotel-room/edit/1 مراجعه کرد، اطلاعات رکورد متناظر با آن نمایش داده شود؛ تا بتواند آن‌ها را ویرایش کند. یعنی می‌خواهیم از همین صفحه، هم برای ویرایش اطلاعات موجود و هم برای ثبت اطلاعات جدید استفاده کنیم. بنابراین نیاز به تعریف مسیریابی دومی در کامپوننت HotelRoomUpsert وجود دارد:
@page "/hotel-room/create"
@page "/hotel-room/edit/{Id:int}"
در اینجا مسیریابی دوم تعریف شده، یک پارامتر مقید شده‌ی به نوع int را انتظار دارد. مزیت این مقید سازی، نمایش خودکار صفحه‌ی «یافت نشد» تعریف شده‌ی در فایل BlazorServer.App\App.razor، با ورود اطلاعاتی غیر عددی است. مسیریابی اول، برای ثبت اطلاعات مورد استفاده قرار می‌گیرد و مسیریابی دوم، برای ویرایش اطلاعات.
پس از تعریف مسیریابی دریافت پارامتر Id رکورد در حال ویرایش، نحوه‌ی واکنش نشان دادن به آن در کامپوننت HotelRoomUpsert.razor به صورت زیر است:
@code
{
    // ...

    [Parameter] public int? Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        else
        {
            // Create Mode
            HotelRoomModel = new HotelRoomDTO();
        }
    }

    // ...
}
- در اینجا پارامتر عددی Id مسیریابی را از نوع nullable تعریف کرده‌ایم. علت اینجا است که اگر کاربر با مسیریابی اول تعریف شده، به این کامپوننت برسد، یعنی قصد ثبت اطلاعات جدیدی را دارد. بنابراین ذکر Id، اختیاری است.
- سپس می‌خواهیم در زمان بارگذاری کامپوننت جاری، اگر مقدار Id، تنظیم شده بود، تمام فیلدهای فرم متصل به شیء HotelRoomModel را به صورت خودکار بر اساس اطلاعات رکورد متناظر با آن در بانک اطلاعاتی، مقدار دهی اولیه کنیم.
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert">
چون فرم جاری توسط کامپوننت EditForm تعریف شده‌است و مدل آن به HotelRoomModel متصل است، بر اساس two-way binding‌های تعریف شده‌ی در اینجا، اگر مقدار Model را به روز رسانی کنیم، به صورت خودکار تمام فیلدهای متصل به آن نیز مقدار دهی اولیه خواهند شد.
کار مقدار دهی اولیه‌ی فیلدهای یک کامپوننت نیز باید در روال رویداد گردان OnInitializedAsync انجام شود که نمونه‌ای از آن را در کدهای فوق مشاهده می‌کنید. در این مثال اگر Id مقدار داشته باشد، مقدار آن‌را به متد GetHotelRoomAsync ارسال کرده و سپس شیء DTO دریافتی از آن‌را به مدل فرم انتساب می‌دهیم تا فرم ویرایشی، قابل استفاده شود:


در ادامه برای ساده سازی رسیدن به مسیرهایی مانند hotel-room/edit/1، به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و در همان ردیفی که اطلاعات رکورد یک اتاق را نمایش می‌دهیم، لینکی را نیز به صفحه‌ی ویرایش آن، اضافه می‌کنیم:
<tr>
    <td>@room.Name</td>
    <td>@room.Occupancy</td>
    <td>@room.RegularRate.ToString("c")</td>
    <td>@room.SqFt</td>
    <td>
        <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink>
    </td>
</tr>
برای این منظور فقط کافی است در جائیکه tr هر رکورد رندر می‌شود، یک td مخصوص NavLink منتهی به hotel-room/edit/{room.Id} را نیز تعریف کنیم:



مدیریت ثبت اطلاعات ویرایش شده‌ی یک اتاق، در بانک اطلاعاتی

در حین تکمیل این قسمت می‌خواهیم پیام‌هایی مانند موفقیت آمیز بودن عملیات را نیز به کاربر نمایش دهیم. به همین جهت مراحل «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» را برای افزودن کتابخانه‌ی معروف جاوا اسکریپتی Toastr طی می‌کنیم که شامل این قسمت‌ها است:
- دریافت و نصب بسته‌های jquery و toastr
- اصلاح فایل Pages\_Host.cshtml برای افزودن مداخل فایل‌های CSS و JS بسته‌های نصب شده
- تعریف فایل جدید wwwroot\js\common.js برای سادگی کار با توابع جاوا اسکریپتی toastr و افزودن مدخل آن به فایل Pages\_Host.cshtml
- تعریف متدهای الحاقی JSRuntimeExtensions ، جهت کاهش کدهای تکراری فراخوانی متدهای toastr و افزودن فضای نام آن به فایل Imports.razor_

جزئیات این موارد را در قسمت یازدهم این سری می‌توانید مطالعه کنید و از تکرار آن‌ها در اینجا صرفنظر می‌شود. همچنین کدهای تکمیل شده‌ی این قسمت را از انتهای مطلب جاری نیز می‌توانید دریافت کنید.

همچنین پیش از تکمیل ادامه‌ی کدهای ویرایش اطلاعات، نیاز است متد IsRoomUniqueAsync را به صورت زیر اصلاح کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomService
    {
        Task<bool> IsRoomUniqueAsync(string name, int roomId);

        // ...
    }
}

namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public Task<bool> IsRoomUniqueAsync(string name, int roomId)
        {
            if (roomId == 0)
            {
                // Create Mode
                return _dbContext.HotelRooms
                                .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                                .AnyAsync(x => x.Name != name);
            }
            else
            {
                // Edit Mode
                return _dbContext.HotelRooms
                                .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                                .AnyAsync(x => x.Name != name && x.Id != roomId);
            }
        }
    }
}
پیشتر در قست 13، بررسی منحصربفرد بودن نام اتاق، از طریق بررسی وجود نام آن در بانک اطلاعاتی صورت می‌گرفت. اینکار در حین ثبت اطلاعات یک رکورد جدید (Id==0) مشکلی را ایجاد نمی‌کند. اما اگر در حال ویرایش اطلاعات باشیم، چون این نام پیشتر ثبت شده‌است، پیام تکراری بودن نام اتاق را دریافت می‌کنیم؛ حتی اگر در اینجا نام اتاق در حال ویرایش را تغییر نداده باشیم و همان نام قبلی باشد. به همین جهت نیاز است در حالت ویرایش اطلاعات، اگر نامی در سایر رکوردها و نه رکوردی با Id مساوی فرم در حال ویرایش، با نام جدید یکی بود، آنگاه آن نام را به صورت تکراری گزارش دهیم که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.

اکنون متد HandleHotelRoomUpsert کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor جهت مدیریت ثبت و ویرایش اطلاعات به صورت زیر تغییر می‌کند:
// ...
@inject IJSRuntime JsRuntime


@code
{
   // ...

    private async Task HandleHotelRoomUpsert()
    {
        var isRoomUnique = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name, HotelRoomModel.Id);
        if (!isRoomUnique)
        {
            await JsRuntime.ToastrError($"The room name: `{HotelRoomModel.Name}` already exists.");
            return;
        }

        if (HotelRoomModel.Id != 0 && Title == "Update")
        {
            // Update Mode
            var updateResult = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
            await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");
        }
        else
        {
            // Create Mode
            var createResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
            await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");
        }

        NavigationManager.NavigateTo("hotel-room");
    }
}
- در ابتدا چون می‌خواهیم پیام‌هایی را توسط Toastr نمایش دهیم، سرویس IJSRuntime را به کامپوننت جاری تزریق کرده‌ایم که این سرویس، امکان دسترسی به متدهای الحاقی ToastrError و ToastrSuccess تعریف شده‌ی در فایل Utils\JSRuntimeExtensions.cs را می‌دهد (کدهای آن در قسمت 11 ارائه شدند).
- در ابتدا عدم تکراری بودن نام اتاق بررسی می‌شود:


- در آخر بر اساس Id مدل فرم، حالت ویرایش و یا ثبت اطلاعات را تشخیص می‌دهیم. البته Id مدل فرم، در حالت ثبت اطلاعات نیز صفر است؛ به علت فراخوانی ()HotelRoomModel = new HotelRoomDTO که سبب مقدار دهی Id آن با عدد پیش‌فرض صفر می‌شود. بنابراین صرفا بررسی Id مدل، کافی نیست و برای مثال می‌توان عنوان تنظیم شده‌ی در متد OnInitializedAsync را نیز بررسی کرد.
- پس از تشخیص حالت ویرایش و یا ثبت، یکی از متدهای متناظر HotelRoom Service را مانند UpdateHotelRoomAsync و یا CreateHotelRoomAsync فراخوانی کرده و سپس پیامی را به کاربر نمایش داده و او را به صفحه‌ی نمایش لیست اتاق‌ها، هدایت می‌کنیم:




کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-15.zip
نظرات مطالب
ارتقاء به HTTP Client در Angular 4.3
یک نکته‌ی تکمیلی
ممکن است در زمان ارسال برخی از درخواست‌ها، نیازی به بررسی CORS  نداشته باشیم؛ مثلا در زمان فراخوانی api ‌های مربوط به google map. در این حالت درخواست باید "simple request" باشد. برای غیرفعال کردن درخواست با متد OPTIONS، در درخواست‌های AJAX  باید شرایط زیر وجود داشته باشد:
1-درخواست نباید شامل HTTP Header ‌های سفارشی باشد. 
2-متد مربوط به درخواست باید یکی از متد‌های Get,Head یا Post  باشد. در صورتیکه درخواست Post باشد، باید Content-Type آن یکی از مقادیر زیر را داشته باشد. 
  1. application/x-www-form-urlencoded 
  2. multipart/form-data 
  3. text/plain 
مثلا در درخواست با متد GET 
this.http.get(url).subscribe(result => {
        const item = result['results'][0];
        this.lat = item.geometry.location.lat;
        this.lng = item.geometry.location.lng;
        this.zoom = 15;
      }, 
      error => {}
);
مطالب
مسیریابی در Angular - قسمت پنجم - تعریف Child Routes
در Angular امکان تعریف مسیریابی‌هایی، درون سایر مسیریابی‌ها نیز پیش بینی شده‌است. با استفاده از مفهوم Child Routes، امکان تعریف سلسله مراتب مسیریابی‌ها جهت ساماندهی و مدیریت مسیریابی درون برنامه، وجود دارد. همچنین lazy loading مسیریابی‌ها را نیز ساده‌تر کرده و کارآیی آغاز برنامه را بهبود می‌بخشند.


علت نیاز به Child Routes
 
در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شده‌است:
<ul class="nav navbar-nav">
      <li><a [routerLink]="['/home']">Home</a></li>
      <li><a [routerLink]="['/products']">Product List</a></li>
      <li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li>      
</ul>
سپس از دایرکتیو router-outlet جهت تعریف محل قرارگیری محتوای این مسیریابی‌ها استفاده شده‌است:
<div class="container">
  <router-outlet></router-outlet>
</div>
هربار که مسیری تغییر می‌کند، محتوای router-outlet با محتوای قالب آن کامپوننت جایگزین خواهد شد. اما اگر تعداد المان‌های صفحه‌ی ویرایش محصولات بیش از اندازه بودند و خواستیم فیلدهای آن‌را به دو برگه (tab) تقسیم کنیم چطور؟ برای اینکار نیاز است تا router-outlet ثانویه و مخصوص این قالب را تعریف کنیم. هربار که کاربری بر روی برگه‌ای کلیک می‌کند، به کمک Child routes، محتوای آن برگه را در این router-outlet ثانویه نمایش می‌دهیم. به این ترتیب به کمک Child routes می‌توان امکان نمایش محتوای مسیریابی دیگری را درون مسیریابی اصلی، میسر کرد.

کاربردهای Child routes
 - امکان تقسیم فرم‌های طولانی به چند Tab
 - امکان طراحی طرحبندی‌های Master/Layout
 - قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
 - بهبود کپسوله سازی ماژول‌های برنامه
 - جزو الزامات Lazy loading هستند


تنظیم کردن Child Routes

مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحه‌ی خوش‌آمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالب‌های کامپوننت‌های این‌ها نیز در router-outlet اصلی برنامه نمایش داده می‌شوند. در ادامه می‌خواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالب‌های کامپوننت‌های هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه می‌کنیم:
>ng g c product/ProductEditInfo
>ng g c product/ProductEditTags
این دستورات سبب به روز رسانی فایل src\app\product\product.module.ts، جهت تکمیل قسمت declarations آن نیز خواهند شد.

به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایه‌ی برچسب‌های یک محصول تکمیل می‌کنیم:
export interface IProduct {
    id: number;
    productName: string;
    productCode: string;
    category: string;
    tags?: string[];
}
در این حالت می‌توانید فایل app\product\product-data.ts را نیز ویرایش کرده و به هر محصول، تعدادی گروه و برچسب را نیز انتساب دهید؛ که البته ذکر tags آن اختیاری است. در اینجا فایل src\app\product\product.service.ts نیز باید ویرایش شده و متد initializeProduct آن تعاریف []:category: null, tags را نیز پیدا کنند.

در ادامه برای تنظیم Child Routes، فایل src\app\product\product-routing.module.ts را گشوده و آن‌را به نحو ذیل تکمیل کنید:
import { ProductEditTagsComponent } from './product-edit-tags/product-edit-tags.component';
import { ProductEditInfoComponent } from './product-edit-info/product-edit-info.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  {
    path: 'products/:id', component: ProductDetailComponent,
    resolve: { product: ProductResolverService }
  },
  {
    path: 'products/:id/edit', component: ProductEditComponent,
    resolve: { product: ProductResolverService },
    children: [
      {
        path: '',
        redirectTo: 'info',
        pathMatch: 'full'
      },
      {
        path: 'info',
        component: ProductEditInfoComponent
      },
      {
        path: 'tags',
        component: ProductEditTagsComponent
      }
    ]
  }
];
- Child Routes، در داخل آرایه‌ی خاصیت children تنظیمات یک مسیریابی والد، قابل تعریف هستند. برای نمونه در اینجا Child Routes به تنظیمات مسیریابی ویرایش محصولات اضافه شده‌اند و کار توسعه‌ی مسیریابی والد خود را انجام می‌دهند.
- در اولین Child Route تعریف شده، مقدار path به '' تنظیم شده‌است. به این ترتیب مسیریابی پیش فرض آن (در صورت عدم ذکر صریح آن‌ها در URL) به صورت خودکار به مسیریابی info هدایت خواهد شد. بنابراین درخواست مسیر products/:id/edit به دومین Child Route تنظیم شده هدایت می‌شود.
- دومین Child Route تعریف شده با مسیری مانند products/:id/edit/info تطابق پیدا می‌کند.
- سومین Child Route تعریف شده با مسیری مانند products/:id/edit/tags تطابق پیدا می‌کند.


تعیین محل نمایش Child Views

برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوه‌ی تعریف آن با primary outlet تعریف شده‌ی در فایل src\app\app.component.html تفاوتی ندارد.
برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آن‌را به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه می‌خواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگه‌ی جدید جایگزین می‌کنیم. در اینجا نحوه‌ی تعریف لینک‌های جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده می‌کنید:
<div class="panel panel-primary">
    <div class="panel-heading">
        {{pageTitle}}
    </div>

    <div class="panel-body" *ngIf="product">
        <div class="wizard">
            <a [routerLink]="['info']">
                Basic Information
            </a>
            <a [routerLink]="['tags']">
                Search Tags
            </a>
        </div>

        <router-outlet></router-outlet>
    </div>

    <div class="panel-footer">
        <div class="row">
            <div class="col-md-6 col-md-offset-2">
                <span>
                    <button class="btn btn-primary"
                            type="button"
                            style="width:80px;margin-right:10px"
                            [disabled]="!isValid()"
                            (click)="saveProduct()">
                        Save
                    </button>
                </span>
                <span>
                    <a class="btn btn-default"
                        [routerLink]="['/products']">
                        Cancel
                    </a>
                </span>
                <span>
                    <a class="btn btn-default"
                        (click)="deleteProduct()">
                        Delete
                    </a>
                </span>
            </div>
        </div>
    </div>

    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
</div>
تا اینجا اگر برنامه را توسط دستور ng s -o اجرا کنید، صفحه‌ی ویرایش محصول اول، چنین شکلی را پیدا کرده‌است:




فعالسازی Child Routes

دو روش برای فعالسازی Child Routes وجود دارند:
الف) با ذکر مسیر مطلق
 <a [routerLink]="['/products',product.id,'edit','info']">Info</a>
در این حالت تمام URL segments این مسیر باید به عنوان پارامترهای لینک قید شوند.

ب) با ذکر مسیر نسبی
 <a [routerLink]="['info']">Info</a>
این مسیر از URL segment جاری شروع می‌شود و نباید در حین تعریف آن از / استفاده کرد. اگر از / استفاده شود، معنای ذکر مسیری مطلق را می‌دهد.
در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار می‌کند).

دقیقا همین پارامترها، قابلیت استفاده‌ی در متد this.route.navigate را نیز دارند:
الف) برای حالت ذکر مسیر مطلق:
 this.router.navigate(['/products', this.product.id,'edit','info']);
ب) و برای حالت ذکر مسیر نسبی:
 this.router.navigate(['info', { relativeTo: this.route }]);
در حالت ذکر مسیر نسبی، نیاز است پارامتر اضافه‌ی دیگری را جهت مشخص سازی مسیریابی والد نیز قید کرد.


تکمیل Child Viewهای برنامه

تا اینجا لینک‌هایی نسبی را به مسیریابی‌های info و tags اضافه کردیم. در ادامه قالب‌ها و کامپوننت‌های آن‌ها را تکمیل می‌کنیم:
الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
import { ActivatedRoute } from '@angular/router';
import { NgForm } from '@angular/forms';
import { Component, OnInit, ViewChild } from '@angular/core';

import { IProduct } from './../iproduct';

@Component({
  //selector: 'app-product-edit-info',
  templateUrl: './product-edit-info.component.html',
  styleUrls: ['./product-edit-info.component.css']
})
export class ProductEditInfoComponent implements OnInit {

  @ViewChild(NgForm) productForm: NgForm;

  errorMessage: string;
  product: IProduct;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];

      if (this.productForm) {
        this.productForm.reset();
      }
    });
  }
}
با قالب src\app\product\product-edit\product-edit.component.html که در حقیقت همان فرمی است که از کامپوننت والد حذف کردیم و به اینجا منتقل شده‌است:
<div class="panel-body">
    <form class="form-horizontal"
          novalidate
          #productForm="ngForm">
        <fieldset>
            <legend>Basic Product Information</legend>
            <div class="form-group" 
                    [ngClass]="{'has-error': (productNameVar.touched || 
                                              productNameVar.dirty || product.id !== 0) && 
                                              !productNameVar.valid }">
                <label class="col-md-2 control-label" 
                        for="productNameId">Product Name</label>

                <div class="col-md-8">
                    <input class="form-control" 
                            id="productNameId" 
                            type="text" 
                            placeholder="Name (required)"
                            required
                            minlength="3"
                            [(ngModel)] = product.productName
                            name="productName"
                            #productNameVar="ngModel" />
                    <span class="help-block" *ngIf="(productNameVar.touched ||
                                                     productNameVar.dirty || product.id !== 0) &&
                                                     productNameVar.errors">
                        <span *ngIf="productNameVar.errors.required">
                            Product name is required.
                        </span>
                        <span *ngIf="productNameVar.errors.minlength">
                            Product name must be at least three characters.
                        </span>
                    </span>
                </div>
            </div>
            
            <div class="form-group" 
                    [ngClass]="{'has-error': (productCodeVar.touched || 
                                              productCodeVar.dirty || product.id !== 0) && 
                                              !productCodeVar.valid }">
                <label class="col-md-2 control-label" for="productCodeId">Product Code</label>

                <div class="col-md-8">
                    <input class="form-control" 
                            id="productCodeId" 
                            type="text" 
                            placeholder="Code (required)"
                            required
                            [(ngModel)] = product.productCode
                            name="productCode"
                            #productCodeVar="ngModel" />
                    <span class="help-block" *ngIf="(productCodeVar.touched ||
                                                     productCodeVar.dirty || product.id !== 0) &&
                                                     productCodeVar.errors">
                        <span *ngIf="productCodeVar.errors.required">
                            Product code is required.
                        </span>
                    </span>
                </div>
            </div>           

            <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
        </fieldset>
    </form>
</div>


ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
import { ActivatedRoute } from '@angular/router';
import { IProduct } from './../iproduct';
import { Component, OnInit } from '@angular/core';

@Component({
  //selector: 'app-product-edit-tags',
  templateUrl: './product-edit-tags.component.html',
  styleUrls: ['./product-edit-tags.component.css']
})
export class ProductEditTagsComponent implements OnInit {

  errorMessage: string;
  newTags = '';
  product: IProduct;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];
    });
  }

  // Add the defined tags
  addTags(): void {
    let tagArray = this.newTags.split(',');
    this.product.tags = this.product.tags ? this.product.tags.concat(tagArray) : tagArray;
    this.newTags = '';
  }

  // Remove the tag from the array of tags.
  removeTag(idx: number): void {
    this.product.tags.splice(idx, 1);
  }
}
با قالب src\app\product\product-edit-tags\product-edit-tags.component.html
<div class="panel-body">
    <form class="form-horizontal"
          novalidate>
        <fieldset>
            <legend>Product Search Tags</legend>
            <div class="form-group" 
                    [ngClass]="{'has-error': (categoryVar.touched || 
                                              categoryVar.dirty || product.id !== 0) && 
                                              !categoryVar.valid }">
                <label class="col-md-2 control-label" for="categoryId">Category</label>
                <div class="col-md-8">
                    <input class="form-control" 
                           id="categoryId" 
                           type="text"
                           placeholder="Category (required)"
                           required
                           minlength="3"
                           [(ngModel)]="product.category"
                           name="category"
                           #categoryVar="ngModel" />
                    <span class="help-block" *ngIf="(categoryVar.touched ||
                                                     categoryVar.dirty || product.id !== 0) &&
                                                     categoryVar.errors">
                        <span *ngIf="categoryVar.errors.required">
                            A category must be entered.
                        </span>
                        <span *ngIf="categoryVar.errors.minlength">
                            The category must be at least 3 characters in length.
                        </span>
                    </span>
                </div>
            </div>

            <div class="form-group" 
                    [ngClass]="{'has-error': (tagVar.touched || 
                                              tagVar.dirty || product.id !== 0) && 
                                              !tagVar.valid }">
                <label class="col-md-2 control-label" for="tagsId">Search Tags</label>
                <div class="col-md-8">
                    <input class="form-control" 
                           id="tagsId" 
                           type="text"
                           placeholder="Search keywords separated by commas"
                           minlength="3"
                           [(ngModel)]="newTags"
                           name="tags"
                           #tagVar="ngModel" />
                    <span class="help-block" *ngIf="(tagVar.touched ||
                                                     tagVar.dirty || product.id !== 0) &&
                                                     tagVar.errors">
                        <span *ngIf="tagVar.errors.minlength">
                            The search tag must be at least 3 characters in length.
                        </span>
                    </span>
                </div>
                <div class="col-md-1">
                    <button type="button"
                            class="btn btn-default"
                            (click)="addTags()">
                        Add
                    </button>
                </div>
            </div>
            <div class="row col-md-8 col-md-offset-2">
                <span *ngFor="let tag of product.tags; let i = index">
                    <button class="btn btn-default"
                            style="font-size:smaller;margin-bottom:12px"
                            (click)="removeTag(i)">
                        {{tag}}
                        <span class="glyphicon glyphicon-remove"></span>
                    </button>
                </span>
            </div>
            <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
        </fieldset>
    </form>
</div>



دریافت اطلاعات جهت Child Routes

روش‌های متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
الف) می‌توان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبل نیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن می‌شود.
ب) می‌توان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آن‌ها از وب سرور، دریافت کرد.
ج) اگر قسمت‌های مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگه‌های مختلف آن به اشتراک گذاشته شوند، این اطلاعات را می‌توانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
در این مثال ما هرچند چندین برگه‌ی مختلف را طراحی کرده‌ایم، اما اطلاعات نمایش داده شده‌ی توسط آن‌ها متعلق به یک شیء محصول می‌باشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننت‌های مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهله‌ی آن کار کرد. به همین جهت با this.route.parent در هر یک از Child Components تعریف شده کار می‌کنیم تا بتوان به یک وهله‌ی شیء محصول، دسترسی یافت.
د) همچنین می‌توان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحه‌ی افزودن محصولات با صفحه‌ی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده می‌کنید.
ngOnInit(): void {
    this.route.parent.data.subscribe(data => {
      this.product = data['product'];

      if (this.productForm) {
        this.productForm.reset();
      }
    });
  }
در اینجا data['product'] به key/value تعریف شده‌ی resolve: { product: ProductResolverService } در تنظیمات مسیریابی اشاره می‌کند که آن‌را در قسمت قبل تکمیل کردیم.
شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفته‌ایم و در آنجا نیز با شیء  this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار می‌کنیم. به این ترتیب مطمئن خواهیم شد که  this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره می‌کنند.
به این ترتیب دکمه‌ی Save ذیل هر دو برگه، به درستی عمل کرده و می‌تواند اطلاعات نهایی یک شیء محصول را ذخیره کند.


رفع مشکلات اعتبارسنجی فرم‌های قرار گرفته‌ی در برگه‌های مختلف

علت استفاده‌ی از ViewChild در ProductEditInfoComponent
 @ViewChild(NgForm) productForm: NgForm;
که به فرم قالب آن اشاره می‌کند:
<form class="form-horizontal" novalidate
#productForm="ngForm">
این است که بتوان متد this.productForm.reset آن‌را پس از هربار دریافت اطلاعات از سرور، فراخوانی کرد. این متد نه تنها اطلاعات آن‌را پاک می‌کند، بلکه خطاهای اعتبارسنجی آن‌را نیز به حالت نخست برمی‌گرداند. بنابراین در این حالت اگر سبب بروز یک خطای اعتبارسنجی، در فرم ویرایش اطلاعات شویم و در همان لحظه صفحه‌ی افزودن یک محصول جدید را درخواست کنیم، کاربر همان خطای اعتبارسنجی قبلی را مجددا مشاهده نکرده و یک فرم از ابتدا آغاز شده را مشاهده می‌کند.
انجام اینکار برای برگه‌‌های دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آن‌ها از حافظه خارج می‌شوند و مجددا بازیابی خواهند شد.

مشکل دوم اعتبارسنجی این فرم چند برگه‌ای این است که هرچند خالی کردن نام یا کد محصول، سبب نمایش خطای اعتبارسنجی می‌شود، اما سبب غیرفعال شدن دکمه‌ی Save نخواهند شد؛ از این جهت که این دکمه در قالب والد قرار دارد و نه در قالب فرزندان.

در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده می‌شود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف می‌شود و با قالب کامپوننت edit tags جایگزین می‌شود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload می‌شوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگه‌ی ویرایش اطلاعات را می‌کند، قالب edit tags و اطلاعات اعتبارسنجی آن unload می‌شوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
راه حل‌‌های ممکن:
الف) بدنه‌ی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المان‌های فرم‌های مرتبط را ارائه دهند. این روش کار نمی‌کند چون Angular المان‌های فرم‌های قرار گرفته‌ی درون router-outlet را شناسایی نمیکند.
ب) قرار دادن فرم‌ها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرم‌های فرزند آن‌را در اینجا ملاحظه می‌کنید:
export class ProductEditComponent implements OnInit {
  private dataIsValid: { [key: string]: boolean } = {};

  isValid(path: string): boolean {
    this.validate();
    if (path) {
      return this.dataIsValid[path];
    }
    return (this.dataIsValid &&
      Object.keys(this.dataIsValid).every(d => this.dataIsValid[d] === true));
  }

  saveProduct(): void {
    if (this.isValid(null)) {
      this.productService.saveProduct(this.product)
        .subscribe(
        () => this.onSaveComplete(`${this.product.productName} was saved`),
        (error: any) => this.errorMessage = <any>error
        );
    } else {
      this.errorMessage = 'Please correct the validation errors.';
    }
  }

  validate(): void {
    // Clear the validation object
    this.dataIsValid = {};

    // 'info' tab
    if (this.product.productName &&
      this.product.productName.length >= 3 &&
      this.product.productCode) {
      this.dataIsValid['info'] = true;
    } else {
      this.dataIsValid['info'] = false;
    }

    // 'tags' tab
    if (this.product.category &&
      this.product.category.length >= 3) {
      this.dataIsValid['tags'] = true;
    } else {
      this.dataIsValid['tags'] = false;
    }
  }
}
 - در اینجا dataIsValid، به صورت key/value تعریف شده‌است که در آن key، مسیر یک برگه و مقدار آن، معتبر بودن یا غیرمعتبر بودن وضعیت اعتبارسنجی آن است.
 - سپس متد validate اضافه شده‌است تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شده‌است برای انجام اعتبارسنجی استفاده می‌کنیم. از این جهت که برگه‌ها نیز با استفاده از  this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگه‌ها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس می‌شود.
 - متد isValid، مسیر هر برگه را دریافت می‌کند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز می‌گرداند. اگر path در اینجا قید نشود، وضعیت تمام برگه‌ها بررسی می‌شوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
 - در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمه‌ی ثبت را نیز به این متد متصل می‌کنیم:
<button class="btn btn-primary"
                            type="button"
                            style="width:80px;margin-right:10px"
                            [disabled]="!isValid()"
                            (click)="saveProduct()">
      Save
</button>


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.