<script type="text/javascript"> $(document).ready(function () { PopupForm.ShowForm({ renderFormUrl : "/postreply/renderreplyform", ..... }); }); </script>
public class MyClass : INotifyPropertyChanged { private string _myValue; public event PropertyChangedEventHandler PropertyChanged; public string MyValue { get { return _myValue; } set { _myValue = value; RaisePropertyChanged("MyValue"); } } protected void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
public class MyDreamClass : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public string MyValue { get; set; } }
پیشنیازها
ابتدا یک برنامه جدید WPF را آغاز کنید. تنظیمات آنرا از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
PM> Install-Package structuremap PM> Install-Package Castle.Core
ساختار برنامه
برنامه ما از یک اینترفیس و کلاس سرویس تشکیل شده است:
namespace AOP01.Services { public interface ITestService { int GetCount(); } } namespace AOP01.Services { public class TestService: ITestService { public int GetCount() { return 10; //این فقط یک مثال است برای بررسی تزریق وابستگیها } } }
using AOP01.Services; using AOP01.Core; namespace AOP01.ViewModels { public class TestViewModel : BaseViewModel { private readonly ITestService _testService; //تزریق وابستگیها در سازنده کلاس public TestViewModel(ITestService testService) { _testService = testService; } // Note: it's a virtual property. public virtual string Text { get; set; } } }
الف) استفاده از کلاس پایه BaseViewModel برای کاهش کدهای تکراری مرتبط با INotifyPropertyChanged که به صورت زیر تعریف شده است:
using System.ComponentModel; namespace AOP01.Core { public abstract class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } }
ج) خاصیتی که در اینجا تعریف شده از نوع virtual است؛ بدون پیاده سازی مفصل قسمت set آن و فراخوانی مستقیم RaisePropertyChanged کلاس پایه به صورت متداول. علت virtual تعریف کردن آن به امکان دخل و تصرف در نواحی get و set این خاصیت توسط Interceptor ایی که در ادامه تعریف خواهیم کرد بر میگردد.
پیاده سازی NotifyPropertyInterceptor
using System; using Castle.DynamicProxy; namespace AOP01.Core { public class NotifyPropertyInterceptor : IInterceptor { public void Intercept(IInvocation invocation) { // متد ست، ابتدا فراخوانی میشود و سپس کار اطلاع رسانی را انجام خواهیم داد invocation.Proceed(); if (invocation.Method.Name.StartsWith("set_")) { var propertyName = invocation.Method.Name.Substring(4); raisePropertyChangedEvent(invocation, propertyName, invocation.TargetType); } } void raisePropertyChangedEvent(IInvocation invocation, string propertyName, Type type) { var methodInfo = type.GetMethod("RaisePropertyChanged"); if (methodInfo == null) { if (type.BaseType != null) raisePropertyChangedEvent(invocation, propertyName, type.BaseType); } else { methodInfo.Invoke(invocation.InvocationTarget, new object[] { propertyName }); } } } }
در اینجا ابتدا اجازه خواهیم داد تا کار set به صورت معمول انجام شود. دو حالت get و set ممکن است رخ دهند. بنابراین در ادامه بررسی خواهیم کرد که اگر حالت set بود، آنگاه متد RaisePropertyChanged کلاس پایه BaseViewModel را یافته و به صورت پویا با propertyName صحیحی فراخوانی میکنیم.
به این ترتیب دیگر نیازی نخواهد بود تا به ازای تمام خواص مورد نیاز، کار فراخوانی دستی RaisePropertyChanged صورت گیرد.
اتصال Interceptor به سیستم
خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شدهاند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آنها مطلع کنیم.
برای این منظور فایل App.xaml.cs را گشوده و در نقطه آغاز برنامه تنظیمات ذیل را اعمال نمائید:
using System.Linq; using System.Windows; using AOP01.Core; using AOP01.Services; using Castle.DynamicProxy; using StructureMap; namespace AOP01 { public partial class App { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); ObjectFactory.Initialize(x => { x.For<ITestService>().Use<TestService>(); var dynamicProxy = new ProxyGenerator(); x.For<BaseViewModel>().EnrichAllWith(vm => { var constructorArgs = vm.GetType() .GetConstructors() .FirstOrDefault() .GetParameters() .Select(p => ObjectFactory.GetInstance(p.ParameterType)) .ToArray(); return dynamicProxy.CreateClassProxy( classToProxy: vm.GetType(), constructorArguments: constructorArgs, interceptors: new[] { new NotifyPropertyInterceptor() }); }); }); } } }
همچنین در ادامه به DI مورد استفاده اعلام میکنیم که ViewModelهای ما دارای کلاس پایه BaseViewModel هستند. بنابراین هر زمانی که این نوع موارد وهله سازی شدند، آنها را یافته و با پروکسی حاوی NotifyPropertyInterceptor مزین کن.
مثالی که در اینجا انتخاب شده، تقریبا مشکلترین حالت ممکن است؛ چون به همراه تزریق خودکار وابستگیها در سازنده کلاس ViewModel نیز میباشد. اگر ViewModelهای شما سازندهای به این شکل ندارند، قسمت تشکیل constructorArgs را حذف کنید.
استفاده از ViewModel مزین شده با پروکسی در یک View
<Window x:Class="AOP01.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </Grid> </Window>
using AOP01.ViewModels; using StructureMap; namespace AOP01 { public partial class MainWindow { public MainWindow() { InitializeComponent(); //علاوه بر تشکیل پروکسی //کار وهله سازی و تزریق وابستگیها در سازنده را هم به صورت خودکار انجام میدهد var vm = ObjectFactory.GetInstance<TestViewModel>(); this.DataContext = vm; } } }
اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که با وارد کردن مقداری در TextBox برنامه، NotifyPropertyInterceptor مورد استفاده قرار میگیرد:
دریافت مثال کامل این قسمت
AOP01.zip
<div id="app"> <div><video #video id="video" width="640" height="480" autoplay></video></div> <div><button id="snap" (click)="capture()">ضبط تصویر</button></div> <canvas #canvas id="canvas" width="640" height="480"></canvas> <ul> <li *ngFor="let capture of captures"> <img src="{{ capture }}" height="50" /> </li> </ul> </div>
export class AppComponent implements OnInit, AfterViewInit { @ViewChild('video') public video: ElementRef; @ViewChild('canvas') public canvas: ElementRef; public captures: Array<any>; public constructor() { this.captures = []; } public ngOnInit() { } public capture() { this.canvas.nativeElement.getContext('2d').drawImage(this.video.nativeElement, 0, 0, 640, 480); this.captures.push(this.canvas.nativeElement.toDataURL('image/png')); } public ngAfterViewInit() { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }).then(stream => { this.video.nativeElement.src = window.URL.createObjectURL(stream); this.video.nativeElement.play(); }); } } }
توضیحات :
با استفاده از local variable هایی که در کدهای HTML تعریف کردیم و ViewChild@، میتوانیم المنتها را در متغیرها، load کنیم. در این حالت این امکان وجود دارد تا المنتهای DOM را دستکاری کنیم.public ngAfterViewInit() { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }).then(stream => { this.video.nativeElement.src = window.URL.createObjectURL(stream); this.video.nativeElement.play(); }); } }
- MediaDevices : این اینترفیس دستیابی به دستگاههای ورودی متصل شده، مثلا دوربین، میکروفن و ... را فراهم میسازد.
- MediaDevices.getUserMedia: با گرفتن مجوزی از کاربر از طریق یک هشدار، دوربین کاربر را روشن میکند و هم چنین این متد یک promise را بازگشت میدهد؛ در صورتیکه کاربر اجازه دسترسی بدهد.
- URL.createObjectURL: یک URL را برای یک BLOB مشخص شده ایجاد میکند ( BLOB: Binary large object ) که میتواند به متدی که انتظار یک URL را دارد، پاس داده شود. بعد از برگشت URL، متد ()revokeObjectURL فراخوانی میشود که کارش آزاد سازی منابع مرتبط با url ایجاد شدهی توسط createObjectURL میباشد. در ضمن طول عمر url ایجاد شده برابر با بستن سند (document) در پنجرهای (window) که در آن ایجاد شدهاست، میباشد.
public capture() { this.canvas.nativeElement.getContext('2d').drawImage(this.video.nativeElement, 0, 0, 640, 480); this.captures.push(this.canvas.nativeElement.toDataURL('image/png')); }
- ()getContext : این متد یک شیء را برگشت میدهد که فراهم کنندهی متدها و خصوصیتها، برای رسم در Canvas میباشد.
- ()drawImage: این متددر Canvas رسم را انجام میدهد و همچنین این متد میتواند بخشهایی از یک تصویر را رسم کند یا سایز یک تصویر را افزایش یا کاهش دهد.
- () toDataURL: این متد یک data URI را بازگشت میدهد که یک تصویر در فرمت مشخص شده را بر اساس پارامتر type، برگشت میدهد (پیش فرض آن png میباشد).
برای تنظیم پویای عنوان یک صفحهی وب، نیاز است با DOM API مرورگر به صورت مستقیم کار کرد. برای مثال فایل wwwroot\main.js را که مدخل آن به کامپوننت Host_ و یا صفحهی index.html اضافه میشود، به صورت زیر تکمیل میکنیم:
window.JsFunctionHelper = { blazorSetTitle: function (title) { document.title = title; } };
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnParametersSetAsync() { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
@page "/counter" <PageTitle Title="@GetPageTitle()" /> <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } private string GetPageTitle() => $"Counter ({currentCount})"; }
برای رفع این مشکل همانطور که در مطلب جاری نیز عنوان شد، باید از روال رویدادگردان OnAfterRenderAsync استفاده کرد. در این حالت کدهای کامپوننت PageTitle.razor به صورت زیر تغییر میکنند:
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
یک نکته: قرار است در Blazor 6x، کامپوننتهای جدید Title، Link و Meta جهت تنظیم اطلاعات تگ head صفحه، به صورت استاندارد اضافه شوند:
<Title Value="@title" /> <Meta name="description" content="Modifying the head from a Blazor component." /> <Link href="main.css" rel="stylesheet" />
تبدیل تعدادی تصویر به یک فایل PDF
ممنون از آموزش مفیدتون. من این سمپل رو برای خودم نوشتم و فقط FitImagesToPage = false قرار دادم اما عکسها به گوشه پایین سمت چپ میچسبه. میخواستم بدونم چکار باید بکنم که عکس در وسط صفحه نمایش داده بشه؟
باتشکر
در این مقاله مروری سریع و کاربردی خواهیم داشت بر تواناییهای مقدماتی LINQ to XML .
فایل Employee.XML را با محتویات زیر در نظر بگیرید:
<Employees>
<Employee>
<Name>Vahid</Name>
<Phone>11111111</Phone>
<Department>IT</Department>
<Age>52</Age>
</Employee>
<Employee>
<Name>Farid</Name>
<Phone>124578963</Phone>
<Department>Civil</Department>
<Age>35</Age>
</Employee>
<Employee>
<Name>Mehdi</Name>
<Phone>1245788754</Phone>
<Department>HR</Department>
<Age>30</Age>
</Employee>
</Employees>
1- چگونه یک فایل XML را جهت استفاده توسط LINQ بارگذاری کنیم؟
قبل از شروع، اسمبلی System.Xml.Linq باید به ارجاعات برنامه اضافه شود. سپس:
using System.Xml.Linq;
XDocument xDoc = XDocument.Load("Employee.xml");
2- اگر محتویات XML دریافتی به صورت رشته بود (مثلا از یک دیتابیس دریافت شد)، اکنون چگونه باید آنرا بارگذاری کرد؟
اینکار را با استفاده از یک StringReader به صورت زیر میتوان انجام داد:
// loading XML from string
StringReader sr = new StringReader(stringXML);
XDocument xDoc = XDocument.Load(sr);
3- چگونه یک کوئری ساده شامل تمامی رکوردهای Employee مجموعه Employees را تهیه کنیم؟
using System.Collections;
IEnumerable<XElement> empList = from e in xDoc.Root.Elements("Employee") select e;
اکنون که مجموعه کارکنان توسط متغیر empList در اختیار ما است، دسترسی به محتویات آن به سادگی زیر خواهد بود:
foreach (XElement employee in empList)
{
foreach (XElement e in employee.Elements())
{
Console.WriteLine(e.Name + " = " + e.Value);
}
}
4- کوئری بنویسید که اطلاعات تمامی کارکنان بخش HR را باز گرداند.
IEnumerable<XElement> hrList = from e in xDoc.Root.Elements("Employee")
where e.Element("Department").Value == "HR"
select e;
همانطور که ملاحظه میکنید همانند عبارات SQL ، در تمامی عناصر متعلق به کارکنان، عناصری که دپارتمان آنها مساوی HR است بازگشت داده میشود.
5- کوئری بنویسید که لیست تمامی کارکنان بالای 30 سال را ارائه دهد.
IEnumerable<XElement> tList = from e in xDoc.Root.Elements("Employee")
where int.Parse(e.Element("Age").Value) > 30
select e;
چون حاصل e.Element("Age").Value یک رشته است، برای اعمال فیلترهای عددی باید این رشتهها تبدیل به عدد شوند. به همین جهت از int.Parse استفاده شده است.
6- کوئری بنویسید که لیست تمامی کارکنان بالای 30 سال را مرتب شده بر اساس نام باز گرداند.
IEnumerable<XElement> tList = from e in xDoc.Root.Elements("Employee")
where int.Parse(e.Element("Age").Value) > 30
orderby e.Element("Name").Value
select e;
7- تبدیل نتیجهی یک کوئری LINQ به لیستی از اشیاء
مفهومی به سی شارپ 3 اضافه شده است به نام anonymous types . برای مثال:
توسط این قابلیت میتوان یک شیء را بدون نیاز به تعریف ابتدایی آن ایجاد کرد و حتی از intelliSense موجود در IDE نیز بهره مند شد. این نوعهای ناشناس توسط واژههای کلیدی new و var تولید میشوند. کامپایلر به صورت خودکار برای هر anonymous type یک کلاس ایجاد میکند.
دقیقا از همین توانایی در LINQ نیز میتوان استفاده نمود:
var empList = from e in xDoc.Root.Elements("Employee")
orderby e.Element("Name").Value
select new
{
Name = e.Element("Name").Value,
Phone = e.Element("Phone").Value,
Department = e.Element("Department").Value,
Age = int.Parse(e.Element("Age").Value)
};
foreach (var employee in empList)
{
Console.WriteLine("Name = " + employee.Name);
Console.WriteLine("Dep = " + employee.Department);
Console.WriteLine("Phone = " + employee.Phone);
Console.WriteLine("Age = " + employee.Age);
}
public class Employee
{
public string Name { get; set; }
public string Phone { get; set; }
public string Department { get; set; }
public int Age { get; set; }
}
برای مثال اگر بخواهیم لیست دریافتی را به صورت یک لیست جنریک بازگشت دهیم خواهیم داشت:
public class Employee
{
public string Name { get; set; }
public string Phone { get; set; }
public string Department { get; set; }
public int Age { get; set; }
}
List<Employee> Get()
{
XDocument xDoc = XDocument.Load("Employee.xml");
var items =
from e in xDoc.Root.Elements("Employee")
orderby e.Element("Name").Value
select new Employee
{
Name = e.Element("Name").Value,
Phone = e.Element("Phone").Value,
Department = e.Element("Department").Value,
Age = int.Parse(e.Element("Age").Value)
};
return items.ToList();
}
<div id="s4-ribbonrow">
<Sharepoint:SPSecurityTrimmedControl runat="server" Permissions="AddDelPrivateWebParts">
و تگ پایانی آن :
نکته مهم در استفاده از این تگ ، ویژگی Permissions آن است که باید با دقت و بسته به نیاز شما تعریف شود :
برخی از این موارد عبارتند از :
EmptyMask – Has no permissions on the Web site. Not available through the user interface.
ViewListItems – View items in lists, documents in document libraries, and view Web discussion comments.
AddListItems – Add items to lists, add documents to document libraries, and add Web discussion comments.
EditListItems – Edit items in lists, edit documents in document libraries, edit Web discussion comments in documents, and customize Web Part Pages in document libraries.
DeleteListItems – Delete items from a list, documents from a document library, and Web discussion comments in documents.
ApproveItems – Approve a minor version of a list item or document.
OpenItems – View the source of documents with server-side file handlers.
ViewVersions – View past versions of a list item or document.
DeleteVersions – Delete past versions of a list item or document.
CancelCheckout – Discard or check in a document which is checked out to another user.
ManagePersonalViews – Create, change, and delete personal views of lists.
ManageLists – Create and delete lists, add or remove columns in a list, and add or remove public views of a list.
ViewFormPages – View forms, views, and application pages, and enumerate lists.
Open – Allow users to open a Web site, list, or folder to access items inside that container.
ViewPages – View pages in a Web site.
AddAndCustomizePages – Add, change, or delete HTML pages or Web Part Pages, and edit the Web site using a SharePoint Foundation–compatible editor.
ApplyThemeAndBorder – Apply a theme or borders to the entire Web site.
ApplyStyleSheets – Apply a style sheet (.css file) to the Web site.
ViewUsageData – View reports on Web site usage.
CreateSSCSite – Create a Web site using Self-Service Site Creation.
ManageSubwebs – Create subsites such as team sites, Meeting Workspace sites, and Document Workspace sites.
CreateGroups – Create a group of users that can be used anywhere within the site collection.
ManagePermissions – Create and change permission levels on the Web site and assign permissions to users and groups.
BrowseDirectories – Enumerate files and folders in a Web site using Microsoft Office SharePoint Designer 2007 and WebDAV interfaces.
BrowseUserInfo – View information about users of the Web site.
AddDelPrivateWebParts – Add or remove personal Web Parts on a Web Part Page.
UpdatePersonalWebParts – Update Web Parts to display personalized information.
ManageWeb – Grant the ability to perform all administration tasks for the Web site as well as manage content. Activate, deactivate, or edit properties of Web site scoped Features through the object model or through the user interface (UI). When granted on the root Web site of a site collection, activate, deactivate, or edit properties of site collection scoped Features through the object model. To browse to the Site Collection Features page and activate or deactivate site collection scoped Features through the UI, you must be a site collection administrator.
UseClientIntegration – Use features that launch client applications; otherwise, users must work on documents locally and upload changes.
UseRemoteAPIs – Use SOAP, WebDAV, or Microsoft Office SharePoint Designer 2007 interfaces to access the Web site.
ManageAlerts – Manage alerts for all users of the Web site.
CreateAlerts – Create e-mail alerts.
EditMyUserInfo – Allows a user to change his or her user information, such as adding a picture.
EnumeratePermissions – Enumerate permissions on the Web site, list, folder, document, or list item.
FullMask – Has all permissions on the Web site. Not available through the user interface.
حال خارج از تگهای SPSecurityTrimmedControl در ابتدا یا انتها ، باید تگ login را مانند زیر به آن اضافه کرد .
و تمام :
درخواست Persia
- فایل ReadMe را فراموش نکنید
حتی اگر پروژه شما از یک سایت اختصاصی استفاده میکند، اولین محلی که عموم کاربران برای دریافت اطلاعات کار با پروژه، به آن مراجعه میکنند، فایل ReadMe برنامه است. این فایل میتواند حاوی مشخصات ذیل باشد:
الف) وابستگیهای پروژه را مشخص کنید
واقعیت این است که برخلاف شمای برنامه نویس، عموم استفاده کنندگان، آشنایی چندانی با جزئیات محیط و شرایط تهیه برنامه شما ندارند. به این ترتیب بسیاری از مسایلی که برای شما بدیهی هستند، برای عموم اینگونه نخواهند بود. بنابراین مسالهای که به سرعت میتواند سبب خشم کاربران و صرفنظر از کار شما گردد، مشخص نبودن نحوه نصب و وابستگیهای لازم برای اجرای برنامه است.
ب) وضعیت بلوغ پروژه خود را مشخص کنید
آیا از این برنامه، مدتی است که در محیط کاری استفاده میکنید؟ آیا به نظر شما هنوز ناتمام است؟ آیا API کتابخانه شما در نگارش بعدی کاملا دگرگون خواهد شد؟ تمام این مسایل و سؤالات را به نحو واضحی توضیح دهید و مشخص کنید. همین توضیحات کوتاه میتوانند ساعتهای بسیاری از زندگی دیگران را صرفه جویی کند.
ج) اگر پروژه شما یک کتابخانه است، نوع زبان و Runtimeهای پشتیبانی شده را مشخص کنید
برای مثال اگر یک کتابخانه دات نتی را ارائه میدهید، مشخص کنید که از کدام نگارش دات نت به بعد را پشتیبانی میکنید.
د) مجوز استفاده از پروژه را مشخص کنید
مطلب مقایسه مجوزهای سورس باز را یکبار مطالعه نمائید و سپس مجوز صحیحی را برای کار خود انتخاب کنید. همچنین آنرا به نحو واضحی در مستندات پروژه خود قید نمائید.
به علاوه بهخاطر داشته باشید که امکان ارائه مجوزهای دوگانه مانند AGPL نیز وجود دارند. در این حالت کاربر یا باید سورس محصول خودش را ارائه دهد، یا مجوز کتابخانه شما را خریداری کند. مانند RavenDB که از این نوع مجوز استفاده میکند.
- یک پروژه نیاز به مستندات دارد
مستند سازی کار، سخت و زمانبر است؛ اما بهترین لطفی است که میتوانید به کاربران خود نمائید. مستندات نه تنها زمان جستجوی بسیاری را صرفه جویی خواهند کرد، همچنین حس اطمینان خاطر را به کاربر القاء میکنند. از این جهت که احساس میکنند شما برای کارتان ارزش قائل بودهاید و احتمال اینکه این برنامه در آینده نزدیک به یک abandonware تبدیل شود، کم است (منظور یک برنامه فراموش شده و خاتمه یافته).
- به روز رسانی را ساده کنید
بالاخره زمانی نیاز خواهد بود تا نگارش جدیدی از کار خود را ارائه دهید. در این حالت نیاز است یک سری از شرایط را مدنظر داشته باشید:
الف) سازگاری قبلی را مدنظر داشته باشید
یکی از بدترین حالات به روز رسانی یک کتابخانه زمانی است که کاربر آن با دهها خطای کامپایل حاصل از به روز رسانی مواجه شود. اگر نیاز است قسمتی از کد خود را حذف کنید یا تغییر دهید، استفاده از ویژگی Obsolete را فراموش نکنید و اینکار باید مرحله به مرحله انجام شود. در یک نگارش، ویژگی Obsolete را معرفی کنید. در دو نگارش بعد، API را تغییر دهید.
ب) حتما یک Change log را تکمیل کنید
پس از ارائه یک نگارش جدید، حداقل در چند سطر مشخص کنید که چه مواردی تغییر کردهاند، چه مواردی اضافه شدهاند و چه مواردی را حذف کردهاید.
همچنین اگر مواردی تغییر کردهاند، نحوه ارتقاء کدهای قدیمی را به نگارش جدید، شرح دهید. اگر مورد جدیدی اضافه شدهاست، لینکی را به مثالی دربارهی آن ارائه دهید.
- نگارشهای جدید را اعلام کنید
برای مثال در طی ارائه یک مطلب جدید در وبلاگ خود، ارائه نگارش جدیدی از کتابخانه یا برنامه خود را به عموم اعلام کنید. در این حالت، حتما لینکی را به change log، ارائه داده و مشخص کنید که وضعیت سازگاری آن با قبل چگونه است.
- محلی را برای دریافت بازخوردهای پروژه خود مشخص کنید
نیاز است بتوانید پروژه خود را پشتیبانی کنید یا به سؤالات مربوطه پاسخ دهید. اگر سورس کنترل یا برنامه مدیریت پروژه شما، امکان پرسش و پاسخ را دارد، که بسیار خوب. اگر خیر، میتوانید مثلا یک گروه گوگل جدید و امثال آنرا برای دریافت بازخوردهای پروژه ایجاد کنید.
همچنین نیاز است لینک به این محل را در فایل ReadME پروژه به صراحت مشخص کنید.
- گذر از پروژه
بالاخره روزی فراخواهد رسید که دیگر علاقهای به نگهداری پروژه نداشته باشید. این مساله را در مکان جمع آوری بازخوردهای خود اعلام کنید یا شخص دیگری را به نگهداری پروژه دعوت نمائید. اگر این کار را انجام ندهید، سبب خواهید شد forkهای متعددی از این پروژه بیجهت ایجاد شده و در نهایت مشخص نباشد که کدامیک بهتر است و کدامیک مشکلات کمتری دارند.
نصب Swashbuckle (سوواَش باکِل)
اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بستهی مختلف مرتبط با آنرا خواهیم یافت. ما در این بین، بیشتر به این بستهها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائهی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامهی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائهی OpenAPI Specification استفاده میکنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شدهی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائهی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز میتوان استفاده کرد.
یک نکته: اگر صرفا بستهی Swashbuckle.AspNetCore را نصب کنیم، هر سه بستهی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آنها استفاده میکنید، انتخاب گزینهی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آنها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی میکند. خلاصهی این موارد، افزودن PackageReference زیر به فایل پروژهی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" /> </ItemGroup> </Project>
تنظیم میانافزار Swashbuckle
پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میانافزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه میکنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( name: "LibraryOpenAPISpecification", info: new Microsoft.OpenApi.Models.OpenApiInfo() { Title = "Library API", Version = "1", Description = "Through this API you can access authors and their books.", Contact = new Microsoft.OpenApi.Models.OpenApiContact() { Email = "name@site.com", Name = "DNT", Url = new Uri("https://www.dntips.ir") }, License = new Microsoft.OpenApi.Models.OpenApiLicense() { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }); }); }
اکنون در متد Configure، میانافزار آنرا خواهیم افزود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); // ... }
تا اینجا اگر برنامه را اجرا کنید، میتوان OpenAPI Specification تولیدی را در آدرس زیر یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.
نگاهی به OpenAPI Specification تولیدی
در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شدهی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهدهاست. سپس لیست مسیرهای این API مشخص شدهاند:
اینها مسیرهایی هستند که توسط دو کنترلر کتابها و نویسندگان برنامهی Web API ما عمومی شدهاند. در اینجا مقابل هر مسیر، تعداد آیتمهای متناظری نیز ذکر شدهاند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده:
که هر کدام به همراه نام متدها و پارامترهای متناظر با آنها نیز میشوند. به علاوه نوع responseهای پشتیبانی شدهی توسط این متدها نیز ذکر شدهاند. هر کدام از خروجیها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آنها بر اساس نوع مدلهای متناظر، استخراج و ارائه شدهاند.
مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست
اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آنرا صرفا 200 یا Ok ذکر کردهاست؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")] public async Task<ActionResult<Author>> GetAuthor(Guid authorId) { var authorFromRepo = await _authorsService.GetAuthorAsync(authorId); if (authorFromRepo == null) { return NotFound(); } return Ok(_mapper.Map<Author>(authorFromRepo)); }
افزودن و راه اندازی Swagger UI
در ادامه میخواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); }); // ... }
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:
و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آنها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شدهاست (از پارامترها تا نوع خروجی):
اکنون اگر بر روی دکمهی try it out آن نیز کلیک کنید، در همینجا میتوان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمهی execute که پس از کلیک بر روی دکمهی try it out ظاهر شدهاست، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.
در انتهای این صفحه، در قسمت schemas آن، مشخصات مدلهای بازگشت داده شدهی توسط Web API نیز ذکر شدهاند:
یک نکته: تغییر آدرس https://localhost:5001/swagger/index.html به ریشهی سایت
اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشهی سایت را در مسیر https://localhost:5001 باز میکنید، Swagger UI نمایان شود، میتوانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); setupAction.RoutePrefix = ""; });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-02.zip
در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.