using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; public class CustomUrlTransformer : IOutboundParameterTransformer { private static readonly Regex _camelCasingRegEx = new Regex("([a-z])([A-Z])", RegexOptions.Compiled); public string TransformOutbound(object value) { return value == null ? null : _camelCasingRegEx.Replace(value.ToString(), "$1-$2"); } }
public class AccountType : ObjectGraphType<Account> { public AccountType() { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object."); Field(x => x.Description).Description("Description property from the account object."); Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object."); } }
public interface IAccountRepository { IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId); } public class AccountRepository : IAccountRepository { private readonly ApplicationContext _context; public AccountRepository(ApplicationContext context) { _context = context; } public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts .Where(a => a.OwnerId.Equals(ownerId)) .ToList(); }
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository) { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object."); Field(x => x.Name).Description("Name property from the owner object."); Field(x => x.Address).Description("Address property from the owner object."); Field<ListGraphType<AccountType>>( "accounts", resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id) ); } }
https://localhost:5001/ui/playground
{ owners{ id, name, address, accounts{ id, description, ownerId } } }
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount> { public AccountTypeEnumType() { Name = "Type"; Description = "Enumeration for the account type object."; } }
public class AccountType : ObjectGraphType<Account> { public AccountType() { ... Field<AccountTypeEnumType>("Type", "Enumeration for the account type object."); } }
{ owners{ id, name, address, accounts{ id, description, type, ownerId } } }
public interface IAccountRepository { ... Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds); }
public class AccountRepository : IAccountRepository { ... public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds) { var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync(); return accounts.ToLookup(x => x.OwnerId); } }
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader) { ... Field<ListGraphType<AccountType>>( "accounts", resolve: context => { var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds); return loader.LoadAsync(context.Source.Id); }); } }
services.AddGraphQL(o => { o.ExposeExceptions = false; }) .AddGraphTypes(ServiceLifetime.Scoped) .AddDataLoader();
public interface IOwnerRepository { ... Owner GetById(Guid id); } public class OwnerRepository : IOwnerRepository { ... Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id)); }
public class AppQuery : ObjectGraphType { public AppQuery(IOwnerRepository repository) { ... Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { var id = context.GetArgument<Guid>("ownerId"); return repository.GetById(id); } ); } }
Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { Guid id; if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id)) { context.Errors.Add(new ExecutionError("Wrong value for guid")); return null; } return repository.GetById(id); } );
{ owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){ id, name, address, accounts{ id, description, type, ownerId } }
string name = context.GetArgument<string>("name");
{ owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") { ... } }
{ first:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }, second:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } } }
fragment SampleName on Type{ ... }
{ first:owners{ ...ownerFields }, second:owners{ ...ownerFields }, ... } fragment ownerFields on OwnerType{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }
query OwnerQuery($ownerId:ID!) { owner(ownerId:$ownerId){ id, name, address, accounts{ id, description, type } } }
{ "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83" }
- یک اپلیکیشن جدید ASP.NET MVC با تنظیمات Individual User Accounts بسازید.
- احراز هویت فیسبوک را توسط کلید هایی که از Facebook دریافت کرده اید فعال کنید. برای اطلاعات بیشتر در این باره میتوانید به این لینک مراجعه کنید.
- برای درخواست اطلاعات بیشتر از فیسبوک، فایل Startup.Auth.cs را مطابق لیست زیر ویرایش کنید.
List<string> scope = newList<string>() { "email", "user_about_me", "user_hometown", "friends_about_me", "friends_photos" }; var x = newFacebookAuthenticationOptions(); x.Scope.Add("email"); x.Scope.Add("friends_about_me"); x.Scope.Add("friends_photos"); x.AppId = "636919159681109"; x.AppSecret = "f3c16511fe95e854cf5885c10f83f26f"; x.Provider = newFacebookAuthenticationProvider() { OnAuthenticated = async context => { //Get the access token from FB and store it in the database and //use FacebookC# SDK to get more information about the user context.Identity.AddClaim( new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken)); } }; x.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie; app.UseFacebookAuthentication(x);
<li> @Html.ActionLink("FacebookInfo", "FacebookInfo","Account") </li>
// // GET: /Account/LinkLoginCallback publicasyncTask<ActionResult> LinkLoginCallback() { var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId()); if (loginInfo == null) { return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); } var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); if (result.Succeeded) { var currentUser = await UserManager.FindByIdAsync(User.Identity.GetUserId()); //Add the Facebook Claim await StoreFacebookAuthToken(currentUser); return RedirectToAction("Manage"); } return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); }
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) { if (User.Identity.IsAuthenticated) { return RedirectToAction("Manage"); } if (ModelState.IsValid) { // Get the information about the user from the external login provider var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) { return View("ExternalLoginFailure"); } var user = newApplicationUser() { UserName = model.Email }; var result = await UserManager.CreateAsync(user); if (result.Succeeded) { result = await UserManager.AddLoginAsync(user.Id, info.Login); if (result.Succeeded) { await StoreFacebookAuthToken(user); await SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } } AddErrors(result); } ViewBag.ReturnUrl = returnUrl; return View(model); }
// // GET: /Account/ExternalLoginCallback [AllowAnonymous] publicasyncTask<ActionResult> ExternalLoginCallback(string returnUrl) { var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); if (loginInfo == null) { return RedirectToAction("Login"); } // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) { //Save the FacebookToken in the database if not already there await StoreFacebookAuthToken(user); await SignInAsync(user, isPersistent: false); return RedirectToLocal(returnUrl); } else { // If the user does not have an account, then prompt the user to create an account ViewBag.ReturnUrl = returnUrl; ViewBag.LoginProvider = loginInfo.Login.LoginProvider; return View("ExternalLoginConfirmation", newExternalLoginConfirmationViewModel { Email = loginInfo.Email }); } }
privateasyncTask StoreFacebookAuthToken(ApplicationUser user) { var claimsIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie); if (claimsIdentity != null) { // Retrieve the existing claims for the user and add the FacebookAccessTokenClaim var currentClaims = await UserManager.GetClaimsAsync(user.Id); var facebookAccessToken = claimsIdentity.FindAll("FacebookAccessToken").First(); if (currentClaims.Count() <=0 ) { await UserManager.AddClaimAsync(user.Id, facebookAccessToken); }
public class FacebookViewModel { [Required] [Display(Name = "Friend's name")] public string Name { get; set; } public string ImageURL { get; set; } }
//GET: Account/FacebookInfo [Authorize] publicasyncTask<ActionResult> FacebookInfo() { var claimsforUser = await UserManager.GetClaimsAsync(User.Identity.GetUserId()); var access_token = claimsforUser.FirstOrDefault(x => x.Type == "FacebookAccessToken").Value; var fb = newFacebookClient(access_token); dynamic myInfo = fb.Get("/me/friends"); var friendsList = newList<FacebookViewModel>(); foreach (dynamic friend in myInfo.data) { friendsList.Add(newFacebookViewModel() { Name = friend.name, ImageURL = @"https://graph.facebook.com/" + friend.id + "/picture?type=large" }); } return View(friendsList); }
@model IList<WebApplication96.Models.FacebookViewModel> @if (Model.Count > 0) { <h3>List of friends</h3> <div class="row"> @foreach (var friend in Model) { <div class="col-md-3"> <a href="#" class="thumbnail"> <img src=@friend.ImageURL alt=@friend.Name /> </a> </div> } </div> }
این یک مثال ساده از کار کردن با تامین کنندگان اجتماعی بود. همانطور که مشاهده میکنید، براحتی میتوانید دادههای بیشتری برای کاربر جاری درخواست کنید و تجربه کاربری و امکانات بسیار بهتری را در اپلیکیشن خود فراهم کنید.
public async Task<bool> IsRoomUniqueAsync(string name, int roomId) { if (roomId == 0) { // Create Mode return !await _dbContext.HotelRooms.AnyAsync(x => x.Name == name); } else { // Edit Mode return !await _dbContext.HotelRooms.AnyAsync(x => x.Name == name && x.Id != roomId); } }
defaultChecks() { const author = { firstName: "Vahid", lastName: "N" }; console.log(author.lastname); author.lastName.trimStart(); author.firstName.charCodeAt("1"); }
- خاصیت lastname در شیء author وجود خارجی ندارد.
- نوع رشتهای، به همراه متد trimStart نیست.
- متد charCodeAt یک عدد را به عنوان پارامتر قبول میکند.
اما باید درنظر داشت که بسیاری از قابلیتهای بررسی کد TypeScript، به صورت پیشفرض فعال نیستند که در ادامه آنها را برای یافتن پیش از موعود بسیاری از مشکلات، فعالسازی خواهیم کرد.
نصب افزونهی TSLint در VSCode
جهت مشاهدهی بهتر خطاهای کامپایلر TypeScript، پیش از کامپایل نهایی کدها، میتوان از افزونهی TSLint استفاده کرد. برای نصب آن، ابتدا باید بستهی ذیل را نصب کرد:
> npm install -g tslint typescript
کار TSLint انجام static code analysis است؛ چیزی شبیه به افزونههایی مانند ریشارپر در ویژوال استودیو که راهنماهایی را در مورد بهتر کردن کیفیت کدهای نوشته شده ارائه میدهد.
فعالسازی بررسی نال و نوعهای نال پذیر
strictNullChecks یکی از مهمترین پرچمهای تنظیمات کامپایلر تایپاسکریپت است. برای افزودن آن، به فایل tsconfig.json مراجعه کرده و پرچم آنرا به true تنظیم کنید:
{ "compilerOptions": { "strictNullChecks": true } }
برای مثال، متد ذیل را در نظر بگیرید:
getSessionItem(key: string): any { const data = window.sessionStorage.getItem(key); return JSON.parse(data); }
[ts] Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'. const data: string | null
برای رفع این مشکل تنها کافی است بررسی کنیم که آیا data نال است یا خیر؟ و اگر خیر، آنگاه آنرا به متد JSON.parse ارسال کنیم:
getSessionItem(key: string): any { const data = window.sessionStorage.getItem(key); if (data) { return JSON.parse(data); } else { return null; } }
گزارش returnهای فراموش شده
در متد ذیل، یک return فراموش شده وجود دارد و تمام شرطهای برنامه به یک خروجی مشخص، منتهی نمیشوند:
noImplicitReturns(a: number) { if (a > 10) { return a; } // No return in this branch }
{ "compilerOptions": { "noImplicitReturns": true } }
[ts] Not all code paths return a value.
تشخیص کدهای مرده
قطعه کدی که پس از یک return قرار بگیرد، یک کد مرده نامیده میشود. با تنظیم پرچم allowUnreachableCode در فایل tsconfig.json به false، میتوان کامپایلر TypeScript را وادار کرد تا اینگونه موارد را به عنوان خطا گزارش کند:
{ "compilerOptions": { "allowUnreachableCode": false } }
allowUnreachableCode() { if (false) { console.log("Unreachable code"); } const a = 1; if (a > 0) { return 10; // reachable code } return 0; console.log("Unreachable code"); }
تشخیص پارامترها و متغیرهای استفاده نشده
دو متد ذیل را درنظر بگیرید:
unusedLocals() { const a = "foo"; // Error: 'a' is declared but its value is never read return "bar"; } unusedParameters(n: number) { n = 0; // Never read }
برای فعالسازی بررسی یک چنین مواردی باید دو پرچم ذیل را در فایل tsconfig.json به true تنظیم کرد:
{ "compilerOptions": { "noUnusedLocals": true, "noUnusedParameters": true } }
[ts] 'a' is declared but its value is never read. [ts] 'n' is declared but its value is never read.
یافتن خواصی که نباید در یک شیء وجود داشته باشند
در مثال ذیل، خاصیت baz در تعاریف اصلی نوعهای x و y وجود ندارد:
excessPropertyForObjectLiterals() { let x: { foo: number }; x = { foo: 1, baz: 2 }; // Error, excess property 'baz' let y: { foo: number, bar?: number }; y = { foo: 1, baz: 2 }; // Error, excess property 'baz' }
{ "compilerOptions": { "suppressExcessPropertyErrors": false } }
[ts] Type '{ foo: number; baz: number; }' is not assignable to type '{ foo: number; }'. Object literal may only specify known properties, and 'baz' does not exist in type '{ foo: number; }'. (property) baz: number
یافتن breakهای فراموش شده در عبارات switch
در مثال زیر، یک break فراموش شدهاست:
fallthroughCasesInSwitchStatement(a: number) { switch (a) { case 0: break; case 1: a += 1; case 2: a += 2; break; } }
{ "compilerOptions": { "noFallthroughCasesInSwitch": true } }
یافتن ایندکسهای تعریف نشدهی در اشیاء
در مثال زیر، شیء x دارای خاصیت b نیست؛ اما دقیقا با این ایندکس مورد استفاده قرار گرفتهاست:
indexingObjectsLackingIndexSignatures() { const x = { a: 0 }; x["a"] = 1; // ok x["b"] = 1; // Error, type '{ a: number; }' has no index signature. }
{ "compilerOptions": { "suppressImplicitAnyIndexErrors": false } }
[ts] Element implicitly has an 'any' type because type '{ a: number; }' has no index signature.
اجبار به تعریف صریح نوعها در TypeScript
عمدهی قابلیت TypeScript در یافتن خطاها به تعاریف نوعها و راهنمایی کامپایلر آن در این زمینه بر میگردد. اما چون این زبان سازگاری کاملی را با JavaScript دارد، تعریف نوعها در آن اجباری نیست و در این حالت اگر نوعی تعریف نشده باشد، به any تفسیر میشود. جهت اجبار به تعریف نوعها در TypeScript میتوان پرچم noImplicitAny را در فایل tsconfig.json به true تنظیم کرد:
{ "compilerOptions": { "noImplicitAny": true } }
noImplicitAny(args) { // Error: Parameter 'args' implicitly has an 'any' type. console.log(args); }
noImplicitAnyArgs(args: string[]) { // ok with the type information console.log(args); }
یک نکتهی تکمیلی
اگر از دستور ng build --watch برای ساخت برنامههای Angular استفاده میکنید، تغییرات فوق زمانی تاثیر داده خواهند شد که یکبار این برنامه را بسته و مجددا اجرا کنید.
- دارای مجوز MIT است. (مجاز هستید از آن در هر نوع برنامهای استفاده کنید)
- cross-platform است. به این معنا که دات نت، WinRT و Xamarin را به خوبی پشتیبانی میکند.
- WPF و همچنین WinForms تا Xamarin.Android را پوشش میدهد.
- بستههای اصلی NuGet آن تا به امروز نزدیک به 40 هزار بار دریافت شدهاند.
- انجمن فعالی دارد.
- بسیار بسیار غنی است. تا حدی که مرور سطحی مجموعه مثالهای آن شاید چند ساعت وقت را به خود اختصاص دهد.
- طراحی آن به نحوی است که با الگوی MVVM کاملا سازگاری دارد.
- به صورت متناوبی به روز شده و نگهداری میشود.
این برنامه (تصویر فوق) که حاوی مرورگر مثالهای آن است، در پوشهی Source\Examples\WPF\ExampleBrowser سورسهای آن قرار دارد.
در ادامه نگاهی خواهیم داشت به نحوهی استفاده از OxyPlot در برنامههای WPF جهت رسم نموداری بلادرنگ که اطلاعات آن در زمان اجرای برنامه تهیه شده و در همین حین نیز تغییر میکنند.
دریافت بستههای نیوگت OxyPlot
برای دریافت دو بستهی OxyPlot.Core و OxyPlot.Wpf تنها کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنیم:
PM> install-package OxyPlot.Wpf
افزودن تعاریف چارت به View
<Window x:Class="OxyPlotWpfTests.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:oxy="http://oxyplot.org/wpf" xmlns:oxyPlotWpfTests="clr-namespace:OxyPlotWpfTests" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <oxyPlotWpfTests:MainWindowViewModel x:Key="MainWindowViewModel" /> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}"> <oxy:PlotView Model="{Binding PlotModel}"/> </Grid> </Window>
ساختار کلی ViewModel برنامه
کار ViewModel متصل شده به View فوق، مقدار دهی PlotModel است.
public class MainWindowViewModel { public PlotModel PlotModel { get; set; }
یک نکتهی کاربردی
اگر هیچ ایدهای نداشتید که این PlotModel را چگونه باید مقدار دهی کرد، به همان برنامهی ExampleBrowser ابتدای مطلب مراجعه کنید.
مثالهای اجرای شدهی آن یک برگهی نمایشی و یک برگهی Code دارند. خروجی این متدها را اگر به خاصیت PlotModel فوق انتساب دهید ... یک چارت کامل خواهید داشت.
مراحل ساخت یک PlotModel
ابتدا نیاز است یک وهلهی جدید از PlotModel را ایجاد کنیم:
private void createPlotModel() { PlotModel = new PlotModel { Title = "سری خطوط", Subtitle = "Pan (right click and drag)/Zoom (Middle click and drag)/Reset (double-click)" }; PlotModel.MouseDown += (sender, args) => { if (args.ChangedButton == OxyMouseButton.Left && args.ClickCount == 2) { foreach (var axis in PlotModel.Axes) axis.Reset(); PlotModel.InvalidatePlot(false); } }; }
برای pan، کافی است دکمهی سمت راست ماوس را نگه داشته و بکشید. به این ترتیب میتوانید نمودار را بر روی محورهای X و Y حرکت دهید.
برای zoom نیاز است دکمهی وسط ماوس را نگه داشته و بکشید. ناحیهای که در این حالت نمایان میگردد، محل بزرگنمایی نهایی خواهد بود.
این دو قابلیت به صورت توکار در OxyPlot قرار دارند و نیازی به کدنویسی برای فعال سازی آنها نیست.
افزودن محورهای X و Y
محور X در مثال ما، از نوع LinearAxis است. بهتر است متغیر آنرا در سطح کلاس تعریف کرد تا بتوان از آن در سایر قسمتهای چارت نیز بهره گرفت:
readonly LinearAxis _xAxis = new LinearAxis(); private void addXAxis() { _xAxis.Minimum = 0; _xAxis.MaximumPadding = 1; _xAxis.MinimumPadding = 1; _xAxis.Position = AxisPosition.Bottom; _xAxis.Title = "X axis"; _xAxis.MajorGridlineStyle = LineStyle.Solid; _xAxis.MinorGridlineStyle = LineStyle.Dot; PlotModel.Axes.Add(_xAxis); }
مقدار دهی GridlineStyleها سبب ایجاد یک Grid خاکستری در نمودار میشوند.
در آخر نیاز است این محور به محورهای PlotModel اضافه شود.
تعریف محور Y نیز به همین نحو است. اگر مقدار خاصیت Position ذکر نشود، این محور در سمت چپ صفحه قرار میگیرد:
readonly LinearAxis _yAxis = new LinearAxis(); private void addYAxis() { _yAxis.Minimum = 0; _yAxis.Title = "Y axis"; _yAxis.MaximumPadding = 1; _yAxis.MinimumPadding = 1; _yAxis.MajorGridlineStyle = LineStyle.Solid; _yAxis.MinorGridlineStyle = LineStyle.Dot; PlotModel.Axes.Add(_yAxis); }
افزودن تعاریف سریهای خطوط
در تصویر فوق، دو سری خط را ملاحظه میکنید. تعاریف پایه سری اول آن به این صورت است:
readonly LineSeries _lineSeries1 = new LineSeries(); private void addLineSeries1() { _lineSeries1.MarkerType = MarkerType.Circle; _lineSeries1.StrokeThickness = 2; _lineSeries1.MarkerSize = 3; _lineSeries1.Title = "Start"; _lineSeries1.MouseDown += (s, e) => { if (e.ChangedButton == OxyMouseButton.Left) { PlotModel.Subtitle = "Index of nearest point in LineSeries: " + Math.Round(e.HitTestResult.Index); PlotModel.InvalidatePlot(false); } }; PlotModel.Series.Add(_lineSeries1); }
هر سری دارای خاصیت MouseDown نیز هست. برای مثال اگر علاقمندید که کلیک کاربر بر روی نقاط مختلف را دریافت کرده و سپس بر این اساس، اطلاعات خاصی را نمایش دهید، میتوانید از مقدار e.HitTestResult.Index استفاده کنید. در اینجا ایندکس نزدیکترین نقطه به محل کلیک کاربر یافت میشود.
تعاریف اولیه سری دوم نیز به همین ترتیب هستند:
readonly LineSeries _lineSeries2 = new LineSeries(); private void addLineSeries2() { _lineSeries2.MarkerType = MarkerType.Circle; _lineSeries2.Title = "End"; _lineSeries2.StrokeThickness = 2; _lineSeries2.MarkerSize = 3; _lineSeries2.MouseDown += (s, e) => { if (e.ChangedButton == OxyMouseButton.Left) { PlotModel.Subtitle = "Index of nearest point in LineSeries: " + Math.Round(e.HitTestResult.Index); PlotModel.InvalidatePlot(false); } }; PlotModel.Series.Add(_lineSeries2); }
به روز رسانی دستی OxyPlot
پس از نمایش اولیه OxyPlot، هر تغییری که در اطلاعات آن صورت گیرد، نمایش داده نخواهد شد. برای به روز رسانی آن فقط کافی است متد PlotModel.InvalidatePlot را فراخوانی نمائید. برای نمونه در متدهای فوق، کلیک ماوس، پس از رسم نمودار انجام میشود. بنابراین اگر نیاز است زیرعنوان نمودار تغییر کند، باید متد PlotModel.InvalidatePlot نیز فراخوانی گردد.
ایجاد یک تایمر برای افزودن نقاط به صورت پویا
در ادامه میخواهیم نقاطی را به صورت پویا به نمودار اضافه کنیم. نمایش یکباره نمودار، نکتهی خاصی ندارد. تنها کافی است توسط lineSeries1.Points.Add یک سری DataPoint را اضافه کنید. این نقاط در زمان نمایش View، به یکباره نمایش داده خواهند شد. اما در اینجا ابتدا یک چارت خالی نمایش داده میشود و سپس قرار است نقاطی به آن اضافه شوند.
private int _xMax; private int _yMax; private bool _haveNewPoints; private void addPoints() { var timer = new DispatcherTimer {Interval = TimeSpan.FromSeconds(1)}; var rnd = new Random(); var x = 1; updateXMax(x); timer.Tick += (sender, args) => { var y1 = rnd.Next(100); updateYMax(y1); _lineSeries1.Points.Add(new DataPoint(x, y1)); var y2 = rnd.Next(100); updateYMax(y2); _lineSeries2.Points.Add(new DataPoint(x, rnd.Next(y2))); x++; updateXMax(x); _haveNewPoints = true; }; timer.Start(); } private void updateXMax(int value) { if (value > _xMax) { _xMax = value; } } private void updateYMax(int value) { if (value > _yMax) { _yMax = value; } }
- افزودن نقاط جدید توسط متدهای lineSeries1.Points.Add انجام میشوند.
- مقادیر max محورهای x و y را نیز ذخیره میکنیم. اگر نقاط برنامه پویا نباشند، OxyPlot به صورت خودکار نمودار را با مقیاس درستی ترسیم میکند. اما اگر نقاط پویا باشند، نیاز است حداکثر محورهای x و y را به صورت دستی در آن تنظیم کنیم. به همین جهت متدهای updateXMax و updateYMax در اینجا فراخوانی شدهاند.
- به روز رسانی ظاهر چارت، توسط متد زیر انجام میشود:
private readonly Stopwatch _stopwatch = new Stopwatch(); private void updatePlot() { CompositionTarget.Rendering += (sender, args) => { if (_stopwatch.ElapsedMilliseconds > _lastUpdateMilliseconds + 2000 && _haveNewPoints) { if (_yMax > 0 && _xMax > 0) { _yAxis.Maximum = _yMax + 3; _xAxis.Maximum = _xMax + 1; } PlotModel.InvalidatePlot(false); _haveNewPoints = false; _lastUpdateMilliseconds = _stopwatch.ElapsedMilliseconds; } }; }
نکتهی دیگری که در متد updatePlot فوق درنظر گرفته شدهاست، تغییر مقدار Maximum محورهای x و y بر اساس حداکثرهای نقاط اضافه شدهاست. به این ترتیب نمودار به صورت خودکار جهت نمایش کل اطلاعات، تغییر اندازه خواهد داد.
البته همانطور که عنوان شد، تمام این تهمیدات برای نمایش نمودارهای بلادرنگ است. اگر کار مقدار دهی Points.Add را فقط یکبار در سازندهی ViewModel انجام میدهید، نیازی به این نکات نخواهید داشت.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
OxyPlotWpfTests.zip
public class SelectAllBehavior { public static bool GetSelectAll(TextBoxBase target) { return (bool)target.GetValue(SelectAllProperty); } public static void SetSelectAll(TextBoxBase target, bool value) { target.SetValue(SelectAllProperty, value); } public static readonly DependencyProperty SelectAllProperty = DependencyProperty.RegisterAttached("SelectAll", typeof(bool), typeof(SelectAllBehavior), new UIPropertyMetadata(false, OnSelectAllPropertyChanged)); static void OnSelectAllPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ((TextBoxBase)o).SelectAll(); } }
<Style TargetType="{x:Type TextBoxBase}" > <Style.Triggers> <Trigger Property="IsFocused" Value="True"> <Setter Property="x: SelectAllBehavior.SelectAll" Value="True"/> </Trigger> </Style.Triggers> </Style>
در تکه کد بالا ،هنگامی که خصوصیت IsFocused مربوط به کنترل TextBox برابر True میشود، یعنی Focus روی TextBox قرار میگیرد،مقدار خصوصیت پیوست شده نیز برابر True میشود که همانطور که گفته شد باعث فراخوانی OnSelectAllPropertyChanged میشود.
تا اینجا فراخوانی یک متد از کنترل از طریق استایل توضیح داده شد، همانطور که در عنوان مطلب آورده شده است. اما اگر بخواهید مثال فوق را به درستی اجرا کنید یعنی هنگام کلیک روی TextBox متن درون آن انتخاب شود، نیاز به اضافه کردن کدهای دیگری وجود دارد. چرا که به صورت پیش فرض زمانی که MouseLeftButtonUp اجرا میشود در صورتی که حالت متن به صورت انتخاب شده باشد، از حالت انتخاب خارج میشود. این بار برای اینکار از خصوصیتهای پیوست شده( AttachedPropery) برای کنترل رویداد PreviewMouseLeftButtonDown استفاده میکنیم و هنگام فشرده شدن کلید سمت چپ موس رویدادهای Click و بقیه LeftMouseButtonDownها را غیرفعال میکنیم.
public class PreviewMouseLeftButtonDownBehavior { public static readonly DependencyProperty PreviewMouseLeftButtonDownProperty = DependencyProperty.RegisterAttached("PreviewMouseLeftButtonDown" , typeof(bool?) , typeof(PreviewMouseLeftButtonDownBehavior) , new UIPropertyMetadata(null, OnPreviewMouseLeftButtonDown) ); public static void SetPreviewMouseLeftButtonDown(DependencyObject target, bool? value) { target.SetValue(PreviewMouseLeftButtonDownProperty, value); } public static object GetPreviewMouseLeftButtonDown(DependencyObject target) { return target.GetValue(PreviewMouseLeftButtonDownProperty); } public static void OnPreviewMouseLeftButtonDown(DependencyObject obj, DependencyPropertyChangedEventArgs e) { var control = obj as Control; if (control == null) return; if (e.NewValue != null && e.OldValue == null) control.PreviewMouseLeftButtonDown += control_PreviewMouseLeftButtonUp; else if ((e.NewValue == null) && (e.OldValue != null)) { control.PreviewMouseLeftButtonDown -= control_PreviewMouseLeftButtonUp; } } static void control_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { var tb = (sender as TextBox); if (tb != null) { if (!tb.IsKeyboardFocusWithin) { e.Handled = true; tb.Focus(); } } } }
و استایل را به صورت زیر تغییر میدهیم:
<Style TargetType="{x:Type TextBoxBase}" > <Setter Property="x:PreviewMouseLeftButtonDownBehavior.IsPreviewMouseLeftButtonDown" Value="True"/> <Style.Triggers> <Trigger Property="IsKeyboardFocusWithin" Value="True"> <Setter Property="InputLanguageManager.InputLanguage" Value="fa-ir" /> </Trigger> <Trigger Property="IsFocused" Value="True"> <Setter Property="xamlServices:SelectAllTextBoxBehavior.SelectAll" Value="True"/> </Trigger> </Style.Triggers> </Style>
در این مثال با Focus رو هر TextBox که استایل فوق را به کار گرفته باشد، متن در حالت انتخاب شده قرار میگیرد. (البته این مشروط به این است که نیاز نباشد رویداد PreviewMouseLeftButtonDown کنترل مورد نظر درون برنامه مقیدسازی(Bind) شود.)
namespace SameInterfaceDifferentClasses.Services.Contracts { public interface IMessageService { void Send(string message); } }
public class EmailService : IMessageService { public void Send(string message) { // ... } } public class SmsService : IMessageService { public void Send(string message) { //todo: ... } }
public interface IUsersManagerService { void ValidateUserByEmail(int id); } public class UsersManagerService : IUsersManagerService { private readonly IMessageService _emailService; private readonly IMessageService _smsService; public UsersManagerService(IMessageService emailService, IMessageService smsService) { _emailService = emailService; _smsService = smsService; } public void ValidateUserByEmail(int id) { _emailService.Send("Validated."); } }
ioc.For<IMessageService>().Use<SmsService>(); ioc.For<IMessageService>().Use<EmailService>();
برای حل این مشکل میتوان به نحو ذیل عمل کرد:
public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(ioc => { // map same interface to different concrete classes ioc.For<IMessageService>().Use<SmsService>(); ioc.For<IMessageService>().Use<EmailService>(); ioc.For<IUsersManagerService>().Use<UsersManagerService>() .Ctor<IMessageService>("smsService").Is<SmsService>() .Ctor<IMessageService>("emailService").Is<EmailService>(); }); } }
var usersManagerService = SmObjectFactory.Container.GetInstance<IUsersManagerService>(); usersManagerService.ValidateUserByEmail(id: 1);
همانطور که در تصویر مشخص است، هر کدام از پارامترها، توسط کلاسهای متفاوتی مقدار دهی شدهاند؛ هرچند از یک اینترفیس مشخص استفاده میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
Dependency-Injection-Samples/DI09
Async Main در C# 7.1
In C# 7.1, the language extends the valid signatures of an entrypoint to allow these async overloads of the Main method to be valid. public static void Main(); public static int Main(); public static void Main(string[] args); public static int Main(string[] args); public static Task Main(); public static Task<int> Main(); public static Task Main(string[] args); public static Task<int> Main(string[] args);
سؤال: آیا میشود دسترسی به فایلهای قرار گرفته در این پوشه عمومی را کنترل کرد؟ به نحوی که فقط کاربران عضو سایت پس از اعتبارسنجی بتوانند آنها را دریافت کنند؟
پاسخ: شاید عنوان کنید که میتوان از تگ location در فایل web.config برای اینکار استفاده کرد:
<location path="Export"> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </location>
سؤال: آیا راه حلی وجود دارد که بتوان فایلهای استاتیک را صرفنظر از نوع، نگارش و حالت اجرای IIS توسط موتور ASP.NET مدیریت کرد؟
پاسخ: بلی. در ASP.NET MVC با تنظیم یک سطر ذیل، اینکار انجام میشود:
public static void RegisterRoutes(RouteCollection routes) { // ... routes.RouteExistingFiles = true; // ... }
RouteCollection در ASP.NET MVC به کمک امکانات MapPathBasedVirtualPathProvider خود، ابتدا درخواست رسیده را بررسی میکند. اگر این درخواست به یک فایل عمومی اشاره کند، کل سیستم مسیریابی را غیرفعال میکند. اما با تنظیم RouteExistingFiles دیگر این بررسی صورت نخواهد گرفت (به عبارتی در بالا بردن سرعت نمایشی سایت نیز تاثیر گذار خواهد بود؛ چون یکی از بررسیها را حذف میکند).
ایجاد کنترلری به نام پوشهای که قرار است محافظت شود
نام پوشه قرار گرفته در ریشه سایت، Export است. بنابراین برای هدایت درخواستهای رسیده به آن (پس از تنظیم فوق)، نیاز است یک کنترلر جدید را به نام Export نیز ایجاد کنیم:
using System.IO; using System.Web.Mvc; namespace Mvc4RouteExistingFiles.Controllers { public class ExportController : Controller { public ActionResult Index(string id) { if (string.IsNullOrWhiteSpace(id)) { return Redirect("/"); } var fileName= Path.GetFileName(id); var path = Server.MapPath("~/export/"+ fileName); return File(path, System.Net.Mime.MediaTypeNames.Application.Octet, fileName); } } }
برای اینکه بررسی کنیم، آیا واقعا فایلهای استاتیک قرار گرفته در پوشه Export به این کنترلر هدایت میشود یا خیر، یک breakpoint را بر روی سطر اول اکشن متد Index قرار میدهیم. برنامه را اجرا کنید ... کار نخواهد کرد، زیرا مسیر یک فایل فرضی به صورت ذیل:
http://localhost/export/test.pdf
برای حل این مشکل فقط کافی است مسیر یابی متناظری را تعریف کنیم:
using System.Web.Mvc; using System.Web.Routing; namespace Mvc4RouteExistingFiles { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.RouteExistingFiles = true; routes.MapRoute( name: "ExportRoute", url: "Export/{id}", defaults: new { controller = "Export", action = "Index", id = UrlParameter.Optional } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } } }
اینبار اگر برنامه را اجرا کنیم، breakpoint ما کار خواهد کرد:
تنظیمات ثانویه پس از فعال سازی RouteExistingFiles
در این حالت با فعال سازی مسیریابی فایلهای موجود، دیگر هیچ فایل استاتیکی به صورت معمول در اختیار کاربران قرار نخواهد گرفت و اگر همانند توضیحات قبل برای آنها کنترلر جداگانهای را تهیه نکنیم، عملا سایت از کار خواهد افتاد.
برای رفع این مشکل، در ابتدای متد RegisterRoutes فوق، تنظیمات ذیل را اضافه کنید تا پوشههای content، scripts و همچنین یک سری فایل با پسوند مشخص، همانند سابق و مستقیما توسط سرور ارائه شوند؛ در غیراینصورت کاربر پیغام 404 را پس از درخواست آنها، دریافت خواهد کرد:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("content/{*pathInfo}"); routes.IgnoreRoute("scripts/{*pathInfo}"); routes.IgnoreRoute("favicon.ico"); routes.IgnoreRoute("{resource}.ico"); routes.IgnoreRoute("{resource}.png"); routes.IgnoreRoute("{resource}.jpg"); routes.IgnoreRoute("{resource}.gif"); routes.IgnoreRoute("{resource}.txt");