This project is the next generation of the ASP.NET Boilerplate web application framework.
Modular Architecture
Designed as modular and extensible from the bottom to the top.
Microservice Focused
Designed to support microservice architecture and helps to build autonomous microservices.
Domain Driven Design
Designed and developed based on DDD patterns and principles. Provides a layered model for your application.
Authorization
Advanced authorization with user, role and fine-grained permission system. Built on the Microsoft Identity library.
Multi-Tenancy
SaaS applications made easy! Integrated multi-tenancy from database to UI.
Cross Cutting Concerns
Complete infrastructure for authorization, validation, exception handling, caching, audit logging, transaction management and so on.
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, cookieClaims, new AuthenticationProperties { // ... RedirectUri = GetSafeRedirectUri(ReturnUrl) });
Unhandled exception rendering component: Invalid non-ASCII or control character in header: 0x062A System.InvalidOperationException: Invalid non-ASCII or control character in header: 0x062A at void Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowInvalidHeaderCharacter(char ch)
در قسمت قبل « کار با اسکنر در برنامههای تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژهی عملی و پیاده سازی مطالب مطرح شده میرویم.
ابتدا پروژهی WCF را شروع میکنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژهی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :
[ServiceContract] public interface IScannerService { [OperationContract] [WebInvoke(Method = "GET", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, UriTemplate = "GetScan")] string GetScan(); }
public class ScannerService : IScannerService { public string GetScan() { // TODO Add code here } }
بر روی پروژهی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینهی Microsoft Windows Image Acquisition Library v2.0 را به پروژهی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر میشود که ارجاعی از آن را در کلاس ScannerService قرار میدهیم.
using WIA;
public string GetScan() { var imgResult = String.Empty; var dialog = new CommonDialogClass(); try { // نمایش فرم پیشفرض اسکنر var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType); // ذخیره تصویر در یک فایل موقت var filename = Path.GetTempFileName(); image.SaveFile(filename); var img = Image.FromFile(filename); // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg); } catch { // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی // بازگردانده میشود که به معنای نبود تصویر میباشد } return imgResult; }
CommonDialogClass کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر میباشد و متدهای مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار میدهد که بسته به نیاز خود میتوانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage میباشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش میدهد و کاربر از طریق آن میتواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :
و سپس فرم پیش فرض اسکنرهای TWAIN را جهت تعیین تنظیمات اسکن نمایش میدهد که این امکان نیز در این فرم فراهم است تا دستگاههای Feeder یا Flated انتخاب گردند.
خروجی این متد همان عکس اسکن شده است که از نوع WIA.ImageFile میباشد و ما پس از دریافتش، ابتدا آن را در یک فایل موقت ذخیره نموده و سپس با استفاده از یک متد کمکی آن را به فرمت Base64 برای درخواست کننده اسکن ارسال مینماییم.
کدهای کلاس کمکی ImageHelper:
public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format) { if (image != null) { using (MemoryStream ms = new MemoryStream()) { // Convert Image to byte[] image.Save(ms, format); byte[] imageBytes = ms.ToArray(); // Convert byte[] to Base64 String string base64String = Convert.ToBase64String(imageBytes); return base64String; } } return String.Empty; }
این مثال به سادهترین شکل نوشته شد. کلاس دیگری هم در اینجا وجود دارد و در صورتیکه از اسکنر نوع Feeder استفاده میکنید، میتوانید از کدهای آن استفاده کنید.
جهت رفع این خطا، در قسمت Referenceهای پروژه خود، WIA را انتخاب نموده و از Propertiesهای آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل میشود.
به سراغ پروژهی ویندوز فرم جهت هاست کردن این WCF سرویس میرویم. میتوانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده میکنیم.
یک پروژهی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژهی قبلی رفته و dllهای آن را به پروژه خود اضافه نمایید.
Form1.cs را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
private readonly Uri _baseAddress = new Uri("http://localhost:6019"); private ServiceHost _host;
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه مینماییم:
private void Form1_Load(object sender, EventArgs e) { _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress); _host.Open(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _host.Close(); }
فایل App.Config پروژهی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="BehaviourMetaData"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="WcfServiceScanner.ScannerService" behaviorConfiguration="BehaviourMetaData"> <endpoint address="" binding="basicHttpBinding" contract="WcfServiceScanner.IScannerService" /> </service> </services> </system.serviceModel> </configuration>
اگر موفق به اجرا نشدید و احیانا با خطای زیر مواجه شدید، اطمینان حاصل کنید که ویژوال استودیو Run as Administrator باشد. مشکل حل خواهد شد.
به سراغ پروژهی بعدی، یعنی وب سایت خود میرویم. یک پروژهی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه میکنیم.
( از آنجا که کدها به صورت جاوا اسکریپت میباشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحهی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a> <img src="" id="img-scanned" /> <script> $("#get-scan").click(function () { var url = 'http://localhost:6019/'; $.get(url, function (data) { $("#img-scanned").attr("src","data:image/Jpeg;base64, "+ data.GetScanResult); }); }); </script>
راه حلهای زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژهی ما جوابگو نمیباشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژهی WCF Service میباشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
public class CORSSupport : IDispatchMessageInspector { Dictionary<string, string> requiredHeaders; public CORSSupport(Dictionary<string, string> headers) { requiredHeaders = headers ?? new Dictionary<string, string>(); } public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty; if (httpRequest.Method.ToLower() == "options") instanceContext.Abort(); return httpRequest; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty; var httpRequest = correlationState as HttpRequestMessageProperty; foreach (var item in requiredHeaders) { httpResponse.Headers.Add(item.Key, item.Value); } var origin = httpRequest.Headers["origin"]; if (origin != null) httpResponse.Headers.Add("Access-Control-Allow-Origin", origin); var method = httpRequest.Method; if (method.ToLower() == "options") httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent; } } // Simply apply this attribute to a DataService-derived class to get // CORS support in that service [AttributeUsage(AttributeTargets.Class)] public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior { #region IServiceBehavior Members void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { var requiredHeaders = new Dictionary<string, string>(); //Chrome doesn't accept wildcards when authorization flag is true //requiredHeaders.Add("Access-Control-Allow-Origin", "*"); requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS"); requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type"); requiredHeaders.Add("Access-Control-Allow-Credentials", "true"); foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher ed in cd.Endpoints) { ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders)); } } } void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } #endregion }
using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher;
[CORSSupportBehavior] public class ScannerService : IScannerService {
کار تمام است، یکبار دیگر ابتدا پروژهی WcfServiecScanner و سپس پروژه هاست را Build کرده و برنامهی هاست را اجرا کنید. اکنون مشاهده میکنید که با زدن دکمهی اسکن، اسکنر فرم تنظیمات اسکن را نمایش میدهد که پس از زدن دکمهی Scan، پروسه آغاز شده و پس از اتمام، تصویر اسکن شده در صفحهی وب سایت نمایش داده میشود.
+ شما در ASP.Net همیشه میتونید نحوه کامپایل شدن را در وب کانفیگ هم تعیین کنید. به این صورت:
compilation defaultLanguage="c#" debug="false"
برای مطالعه بیشتر
http://aspnetresources.com/articles/debug_code_in_production.aspx
و یا استفاده از روش زیر که در آن retail را باید true کنید:
http://msdn.microsoft.com/en-us/library/ms228298(VS.80).aspx
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شدهاست، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر میکند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحهای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام میشود. از این رو با توجه به داشتن Urlهای منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف میکنیم.
برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانهی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن مینماییم:
Install-Package MvcAjaxPager
در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار میدهیم:
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم :
public class Topic { public int Id; public string Title; public string Text; }
public class TopicService { public static IEnumerable<Topic> Topics = new List<Topic>() { new Topic{Id=1,Title="Title 1",Text= "Text 1"}, new Topic{Id=2,Title="Title 2",Text="Text 2"}, new Topic{Id=3,Title="Title 3",Text="Text 3"}, new Topic{Id=4,Title="Title 4",Text="Text 4"}, new Topic{Id=5,Title="Title 5",Text="Text 5"}, new Topic{Id=6,Title="Title 6",Text="Text 6"}, new Topic{Id=7,Title="Title 7",Text="Text 7"}, new Topic{Id=8,Title="Title 8",Text="Text 8"}, new Topic{Id=9,Title="Title 9",Text="Text 9"}, new Topic{Id=10,Title="Title 10",Text="Text 10"}, new Topic{Id=11,Title="Title 11",Text="Text 11"}, new Topic{Id=12,Title="Title 12",Text="Text 12"}, new Topic{Id=13,Title="Title 13",Text="Text 13"}, new Topic{Id=14,Title="Title 14",Text="Text 14"}, new Topic{Id=15,Title="Title 15",Text="Text 15"}, new Topic{Id=16,Title="Title 16",Text="Text 16"}, new Topic{Id=17,Title="Title 17",Text="Text 17"}, new Topic{Id=18,Title="Title 18",Text="Text 18"}, new Topic{Id=19,Title="Title 19",Text="Text 19"}, new Topic{Id=20,Title="Title 20",Text="Text 20"}, new Topic{Id=21,Title="Title 21",Text="Text 21"}, new Topic{Id=22,Title="Title 22",Text="Text 22"}, }; public static IEnumerable<Topic> GetAll() { return Topics.OrderBy(row => row.Id); } }
public class ListViewModel { public IEnumerable<Topic> Topics { get; set; } public int PageIndex { get; set; } public int TotalItemCount { get; set; } }
public ActionResult Index(int page = 1) { var topics = TopicService.GetAll (); int totalItemCount = topics.Count(); var model = new ListViewModel() { PageIndex = page, Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(), TotalItemCount = totalItemCount }; if (!Request.IsAjaxRequest()) { return View(model); } return PartialView("_TopicList", model); }
و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager @model ListViewModel @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = false, CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null) <table> <tr> <th> @Html.DisplayName("ID") </th> <th> @Html.DisplayName("Title") </th> <th> @Html.DisplayName("Text") </th> </tr> @foreach (var topic in Model.Topics) { <tr> <td> @topic.Id </td> <td> @topic.Title </td> <td> @topic.Text </td> </tr> } </table> @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = true, FirstPageText = "اولین", LastPageText = "آخرین", MorePageText = "...", PrevPageText = "قبلی", NextPageText = "بعدی", CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null)
حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11 OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکوردها، PageSize تعداد رکوردهای هر صفحه و PageIndex آدرس صفحه جاری میباشد.
مهمترین بخش این pager که قابلیتهای زیادی را به کاربر میدهد، قسمت PagerOptions آن است و تعدادی از پارامترهای آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ، AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود.
AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id جهت تعیین Id خاص
و در انتها، در view مربوطه داریم:
@using MvcAjaxPager @model ListViewModel @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div id="dvTopics"> @{ @Html.Partial("_TopicList", Model); } </div> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script> <script type="text/javascript"> $('.NavigationBox').pager(); //pagination before start function AjaxStart() { console.log('Start AJAX call. Loading message can be shown'); } // pagination - after request function AjaxStop() { console.log('Stop AJAX call. Loading message can be hidden'); }; </script> </body> </html>
BloggerToCHM
چند حدس:
- html help work shop نصب نشده؟
- html help work shop نصب شده ولی مسیری که از رجیستری گرفته میشه غلط هست و فایل نهایی ساخته نمیشه.
به منوی فایل گزینه preferences مراجعه کنید، گزینه delete temp files را false کنید و save . الان یکبار دیگر db to chm رو تست کنید. فایلهای موقتی حذف نخواهند شد. یک مجموعه کامل تولید میشود که فایل blog.hhp آن در html help work shop قابل گشودن و کامپایل است (روش دستی).
برنامه به صورت خودکار دنبال این مسیر میگردد:
HKEY_CURRENT_USER\Software\Microsoft\HTML Help Workshop
لطفا بررسی کنید آیا این مسیر اصلا در رجیستری ویندوز هست؟ آیا InstallDir آن درست مقدار دهی شده؟
با تشکر
تبادل داده ها بین لایه ها- قسمت اول
آیا استفاده از FullName یک کلاس تضادی با Clean Code دارد؟
آنچه که در خصوص global using فرمودید و با توجه به تشابه اسامی کلاسها، باید از Alias استفاده کنم و در زمان استفاده لزوما بجای استفاده از آدرس کامل و طولانی فضای نام، از نام Alias استفاده کنم.