column.ColumnItemsTemplate(template => { template.ProgressBar(progressBarColor: Color.SkyBlue, showPercentText: true); template.DisplayFormatFormula(obj => obj == null ? string.Format("{0:P2}", 0) : string.Format("{0:P2}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Average); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Format("{0:P2}", 0) : string.Format("{0:P2}", obj)); }); column.CalculatedField( list => { if (list == null) return string.Empty; float sumCountPercent = 0; float count = 1; foreach (var item in IndexData) { sumCountPercent += float.Parse(list.GetValueOf(item.Value).ToString()) * (count++ * 20); } float sumColumnsPercent = float.Parse(list.GetValueOf("SumColumns").ToString()) * 100; return sumCountPercent / sumColumnsPercent; });
- خواندن متن متدهای بالادستی را آسانتر میکند. در واقع نام متد عملا نقش کامنت را ایفا میکند. مراجعه کنید به Self documenting code
- احتمال اینکه متدها دوباره استفاده شوند را بالا میبرد
public void PrintInfo(dynamic person) { Console.WriteLine(person.Name); Console.WriteLine(person.LastName); Console.WriteLine(person.Job.Title); Console.WriteLine(person.Job.Salary); }
public void PrintInfo(dynamic person) { Console.WriteLine(person.Name); Console.WriteLine(person.LastName); PrintJob(person.Job); } public void PrintJob(dynamic job) { Console.WriteLine(job.Title); Console.WriteLine(job.Salary); }
مراحل انجام این بازسازی کد
- متد جدیدی ایجاد کنید و نام آن را بر اساس کاری که قرار است تکه کد انتخاب شده انجام دهد، تعیین کنید (نام را بر اساس کاری که قرار است انجام دهد انتخاب نمایید؛ نه نحوه انجام دادن آن کار).
- تکه کد نیازمند استخراج را از بدنه متد قبلی به بدنه متد جدید کپی کنید.
- کد استخراج شده را برای پیدا کردن متغیرهای محلی بهجا مانده از متد اصلی بررسی کنید. این متغیرها احتمالا متغیرهای محلی متد جدید و پارامترهای آن را تشکیل خواهند داد.
- اگر متغیرهای محلی موجود در تکه کد استخراج شده فقط در آن استفاده شدهاند، آنها را به صورت متغیرهای محلی موقتی در متد جدید ایجاد کنید.
- متغیرهای محلی تکه کد استخراج شده را بررسی کنید که آیا در جایی از آن ویرایش شدهاند یا خیر؟ در صورتیکه ویرایش شده باشند، بررسی کنید که آیا میتوان آن را به عنوان خروجی متد جدید در نظر گرفت و متغیرها را مقداردهی کرد یا خیر؟ (توضیح بیشتر در مثال)
- پارامترهای متناظری را با متغیرهای محلی موقتی ایجاد شده، در متد جدید ایجاد کنید.
- بعد از این که تکلیف تمامی متغیرهای محلی مشخص شد، کد را کامپایل کنید.
- در متد مبدا تکه کد استخراج شده را به فراخوانی متد جدید تغییر دهید.
- کد را کامپایل و تست کنید .
- متغیر ویرایش شده فقط در تکه کد استخراج شده ویرایش و استفاده شدهاست.
- متغیر ویرایش شده بعد از تکه کد استخراج شده نیز در بدنه متد اصلی مورد استفاده قرار گرفتهاست.
public void SetPayableAmount(dynamic orderItem) { var price = orderItem.Price; var discount = 0; var discountPlan = GetDiscountPlan(); var discountPercentage = discountPlan.Percentage; discount = price * discountPercentage; orderItem.PayableAmount = orderItem.Price - discount } private dynamic GetDiscountPlan() { throw new NotImplementedException(); }
public void SetPayableAmount(dynamic orderItem) { var price = orderItem.Price; var discount = GetDiscount(price); orderItem.PayableAmount = orderItem.Price - discount; } public dynamic GetDiscount(decimal price) { var discountPlan = GetDiscountPlan(); var discountPercentage = discountPlan.Percentage; return price * discountPercentage; } private dynamic GetDiscountPlan() { throw new NotImplementedException(); }
ابزارهای کمکی برای این بازسازی کد
public async Task Invoke(HttpContext context) { if (!_memoryCache.TryGetValue("OnlineUsers", out Dictionary<string,DateTime> onlineUsers)) { onlineUsers = new Dictionary<string, DateTime>(); _memoryCache.Set("OnlineUsers", onlineUsers , new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }); } if (context.User.Identity.IsAuthenticated) { var name = context.User.Identity.Name; if (name != null) { if (onlineUsers.ContainsKey(name)) onlineUsers[name] = DateTime.Now; else onlineUsers.Add(name, DateTime.Now); } } await _next(context); }
آشنایی با مدل برنامه نویسی TAP
دات نت فریم ورک، از زمان ارائه نگارش یک آن، از اعمال غیرهمزمان و API خاص آن پشتیبانی میکردهاست. همچنین این مورد یکی از ویژگیهای Win32 نیز میباشد. نوشتن کدهای همزمان متداول بسیار ساده است. در این نوع کدها هر عملیات خاص، پس از پایان عملیات قبلی انجام میشود.
public string TestNoneAsync() { var webClient = new WebClient(); return webClient.DownloadString("http://www.google.com"); }
این مورد همچنین در برنامههای سمت سرور نیز حائز اهمیت است. با قفل شدن تعداد زیادی ترد در حال اجرا، عملا قدرت پاسخدهی سرور نیز کاهش مییابد. بنابراین در این نوع موارد، برنامههای چند ریسمانی هرچند در سمت کلاینت ممکن است مفید واقع شوند و برای مثال ترد UI را آزاد کنند، اما اثر آنچنانی بر روی برنامههای سمت سرور ندارند. زیرا در آنها میتوان هزاران ترد را ایجاد کرد که همگی دارای کدهای اصطلاحا blocking باشند. برای حل این مساله استفاده از API غیرهمزمان توصیه میشود.
برای نمونه کلاس WebClient توکار دات نت، دارای متدی به نام DownloadStringAsync نیز میباشد. این متد به محض فراخوانی، ترد جاری را آزاد میکند. به این معنا که فراخوانی آن سبب توقف ترد جاری برای دریافت نتیجهی دریافت اطلاعات از وب نمیشود. به این نوع API، یک Asynchronous API گفته میشود؛ زیرا با سایر کدهای نوشته شده، هماهنگ و همزمان اجرا نمیشود.
هر چند این کد جدید مشکل عدم پاسخ دهی برنامه را برطرف میکند، اما مشکل دیگری را به همراه دارد؛ چگونه باید حاصل عملیات آنرا پس از پایان کار دریافت کرد؟ چگونه باید خطاها و مشکلات احتمالی را مدیریت کرد؟
برای مدیریت این مساله، رخدادی به نام DownloadStringCompleted تعریف شدهاست. روال رویدادگردان آن پس از پایان کار دریافت اطلاعات از وب، فراخوانی میگردد.
public void TestAsync() { var webClient = new WebClient(); webClient.DownloadStringAsync(new Uri("http://www.google.com")); webClient.DownloadStringCompleted += webClientDownloadStringCompleted; } void webClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { // use e.Result }
مشکل! ما سادگی یک عملیات همزمان را از دست دادیم. متد TestNoneAsync از لحاظ پیاده سازی و همچنین خواندن و نگهداری آن در طول زمان، بسیار سادهتر است از نمونهی TestAsync نوشته شده. در کدهای غیرهمزمان فوق، یک متد ساده، به دو متد مجزا خرد شدهاست و نتیجهی نهایی، درون یک روال رخدادگردان بدست میآید.
به این مدل، EAP یا Event based asynchronous pattern نیز گفته میشود. EAP در دات نت 2 معرفی شد. روالهای رخدادگردان در این حالت، در ترد اصلی برنامه اجرا میشوند. اما اگر به حالت اصلی اعمال غیرهمزمان موجود از دات نت یک کوچ کنیم، اینطور نیست. در WinForms و WPF برای به روز رسانی رابط کاربری نیاز است اطلاعات دریافت شده در همان تردی که رابط کاربری ایجاد شده است، تحویل گرفته شده و استفاده شوند. در غیراینصورت استثنایی صادر شده و برنامه خاتمه مییابد.
آشنایی با Synchronization Context
ابتدا یک برنامهی WinForms ساده را آغاز کرده و یک دکمهی جدید را به نام btnGetInfo و یک تکست باکس را به نام txtResults، به آن اضافه کنید. سپس کدهای فرم اصلی آنرا به نحو ذیل تغییر دهید:
using System; using System.Linq; using System.Net; using System.Windows.Forms; namespace Async02 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnGetInfo_Click(object sender, EventArgs e) { var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); txtResults.Text = headersText; }, null); } private string formatHeaders(WebHeaderCollection headers) { var headerString = headers.Keys.Cast<string>() .Select(header => string.Format("{0}:{1}", header, headers[header])); return string.Join(Environment.NewLine, headerString.ToArray()); } } }
همچنین در این مثال از متد غیرهمزمان BeginGetResponse نیز استفاده شدهاست. در این نوع API خاص، کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید.
اگر برنامه را اجرا کنید، با استثنای زیر مواجه خواهید شد:
An exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll but was not handled in user code Additional information: Cross-thread operation not valid: Control 'txtResults' accessed from a thread other than the thread it was created on.
private void btnGetInfo_Click(object sender, EventArgs e) { var sync = SynchronizationContext.Current; var req = (HttpWebRequest)WebRequest.Create("http://www.google.com"); req.Method = "HEAD"; req.BeginGetResponse( asyncResult => { var resp = (HttpWebResponse)req.EndGetResponse(asyncResult); var headersText = formatHeaders(resp.Headers); sync.Post(delegate { txtResults.Text = headersText; }, null); }, null); }
برای درک بهتر آن، سه break point را پیش از متد BeginGetResponse، داخل Async calback و داخل delegate متد Post قرار دهید. پس از اجرای برنامه، از منوی دیباگ در VS.NET گزینهی Windows و سپس Threads را انتخاب کنید.
در اینجا همانطور که مشخص است، کد داخل delegate تعریف شده، در ترد اصلی برنامه اجرا میشود و نه یکی از Worker threadهای ثانویه.
هر چند استفاده از متدهای تو در تو و lambda syntax، نیاز به تعریف چندین متد جداگانه را برطرف کردهاست، اما باز هم کد سادهای به نظر نمیرسد. در سی شارپ 5، برای مدیریت بهتر تمام مشکلات یاد شده، پشتیبانی توکاری از اعمال غیرهمزمان، به هستهی زبان اضافه شدهاست.
Syntax ابتدایی یک متد Async
در ابتدا کلاس و متد Async زیر را در نظر بگیرید:
using System; using System.Threading.Tasks; namespace Async01 { public class AsyncExample { public async Task DoWorkAsync(int parameter) { await Task.Delay(parameter); Console.WriteLine(parameter); } } }
- در مدل برنامه نویسی TAP، متدهای غیرهمزمان باید یک Task را بازگشت دهند؛ یا نمونهی جنریک آنرا. البته کامپایلر، async void را نیز پشتیبانی میکند ولی در قسمتهای بعدی بررسی خواهیم کرد که چرا استفاده از آن مشکلزا است و باید از آن پرهیز شود.
- همچنین مطابق TAP، اینگونه متدها باید به پسوند Async ختم شوند تا استفاده کننده در حین کار با Intellisense، بتواند آنها را از متدهای معمولی سریعتر تشخیص دهد.
- از واژهی کلیدی async نیز استفاده میگردد تا کامپایلر از وجود اعمال غیر همزمان مطلع گردد.
- await به کامپایلر میگوید، عبارت پس از من، یک وظیفهی غیرهمزمان است و ادامهی کدهای نوشته شده، تنها زمانی باید اجرا شوند که عملیات غیرهمزمان معرفی شده، تکمیل گردد.
در متد DoWorkAsync، ابتدا به اندازهای مشخص توقف حاصل شده و سپس سطر بعدی یعنی Console.WriteLine اجرا میشود.
یک اشتباه عمومی! استفاده از واژههای کلیدی async و await متد شما را async نمیکنند.
برخلاف تصور ابتدایی از بکارگیری واژههای کلیدی async و await، این کلمات نحوهی اجرای متد شما را async نمیکنند. این کلمات صرفا برای تشکیل متدهایی که هم اکنون غیرهمزمان هستند، مفید میباشند. برای توضیح بیشتر آن به مثال ذیل دقت کنید:
public async Task<double> GetNumberAsync() { var generator = new Random(); await Task.Delay(generator.Next(1000)); return generator.NextDouble(); }
در ادامه برای استفاده از آن خواهیم داشت:
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
در کدهای همزمان متداول، سطر اول ابتدا انجام میشود و بعد سطر دوم و الی آخر. با استفاده از واژهی کلیدی await یک چنین عملکردی را با اعمال غیرهمزمان خواهیم داشت. پیش از این برای مدیریت اینگونه اعمال از یک سری callback و یا رخداد استفاده میشد. برای مثال ابتدا عملیات همزمانی شروع شده و سپس نتیجهی آن در یک روال رخداد گردان جایی در کدهای برنامه دریافت میشد (مانند مثال ابتدای بحث). اکنون تصور کنید که قصد داشتید جمع نهایی حاصل دو عملیات غیرهمزمان را از دو روال رخدادگردان جدا از هم، جمع آوری کرده و بازگشت دهید. هرچند اینکار غیرممکن نیست، اما حاصل کار به طور قطع آنچنان زیبا نبوده و قابلیت نگهداری پایینی دارد. واژهی کلیدی await، انجام اینگونه امور غیرهمزمان را طبیعی و همزمان جلوه میدهد. به این ترتیب بهتر میتوان بر روی منطق و الگوریتمهای مورد استفاده تمرکز داشت، تا اینکه مدام درگیر مکانیک اعمال غیرهمزمان بود.
امکان استفاده از واژهی کلیدی await در هر جایی از کدها وجود دارد. برای نمونه در مثال زیر، برای ترکیب دو عملیات غیرهمزمان، از await در حین تشکیل عملیات ضرب نهایی، دقیقا در جایی که مقدار متد باید بازگشت داده شود، استفاده شدهاست:
public async Task<double> GetProductOfSumAsync() { var leftOperand = GetSumAsync(); var rightOperand = GetSumAsync(); return await leftOperand * await rightOperand; }
Operator '*' cannot be applied to operands of type 'System.Threading.Tasks.Task<double>' and 'System.Threading.Tasks.Task<double>'
اگر متد DownloadString همزمان ابتدای بحث را نیز بخواهیم تبدیل به نمونهی async سیشارپ 5 کنیم، میتوان از متد الحاقی جدید آن به نام DownloadStringTaskAsync کمک گرفت:
public async Task<string> DownloadAsync() { var webClient = new WebClient(); return await webClient.DownloadStringTaskAsync("http://www.google.com"); }
سؤال: آیا استفاده از await نیز ترد جاری را قفل میکند؟
اگر به کدها دقت کنید، استفاده از await به معنای صبر کردن تا پایان عملیات async است. پس اینطور به نظر میرسد که در اینجا نیز ترد اصلی، همانند قبل قفل شدهاست.
public void TestDownloadAsync() { Debug.WriteLine("Before DownloadAsync"); DownloadAsync(); Debug.WriteLine("After DownloadAsync"); }
Before DownloadAsync After DownloadAsync
برنامههای Async و نگارشهای مختلف دات نت
شاید در ابتدا به نظر برسد که قابلیتهای جدید async و await صرفا متعلق هستند به دات نت 4.5 به بعد؛ اما خیر. اگر کامپایلری را داشته باشید که از این واژههای کلیدی را پشتیبانی کند، امکان استفاده از آنها را با دات نت 4 نیز خواهید داشت. برای این منظور تنها کافی است از VS 2012 به بعد استفاده نمائید. سپس در کنسول پاورشل نیوگت دستور ذیل را اجرا نمائید (فقط برای برنامههای دات نت 4 البته):
PM> Install-Package Microsoft.Bcl.Async
آماده سازی یک مثال Self host
برای اینکه خروجیهای JSON را بهتر و بدون نیاز به ابزار خاصی مشاهده کنیم، میتوان یک پروژهی کنسول جدید را آغاز کرده و سپس آنرا تبدیل به Host مخصوص Web API کرد. برای اینکار تنها کافی است در کنسول پاور شل نیوگت دستور ذیل را صادر کنید:
PM> Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; namespace WebApiSelfHostTests { public class UsersController : ApiController { public IEnumerable<User> GetAllUsers() { return new[] { new User{ Id = 1, Name = "User 1", Type = UserType.Admin }, new User{ Id = 2, Name = "User 2", Type = UserType.User } }; } public async Task<HttpResponseMessage> Post(HttpRequestMessage request) { var jsonContent = await request.Content.ReadAsStringAsync(); Console.WriteLine("JsonContent (Server Side): {0}", jsonContent); return new HttpResponseMessage(HttpStatusCode.Created); } } }
namespace WebApiSelfHostTests { public enum UserType { User, Admin, Writer } public class User { public int Id { set; get; } public string Name { set; get; } public UserType Type { set; get; } } }
using System.Web.Http; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Owin; namespace WebApiSelfHostTests { /// <summary> /// PM> Install-Package Microsoft.AspNet.WebApi.OwinSelfHost /// </summary> public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); appBuilder.UseWebApi(config); } } }
var server = WebApp.Start<Startup>(url: BaseAddress); Console.WriteLine("Press Enter to quit."); Console.ReadLine(); server.Dispose();
using (var client = new HttpClient()) { var response = client.GetAsync(BaseAddress + "api/users").Result; Console.WriteLine("Response: {0}", response); Console.WriteLine("JsonContent (Client Side): {0}", response.Content.ReadAsStringAsync().Result); }
JsonContent (Client Side): [{"Id":1,"Name":"User 1","Type":1},{"Id":2,"Name":"User 2","Type":0}]
تنظیمات JSON سمت سرور Web API
برای تغییر این خروجی، در سمت سرور تنها کافی است به کلاس Startup مراجعه و HttpConfiguration را به صورت ذیل تنظیم کنیم:
public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Converters = { new StringEnumConverter() } };
اینبار اگر برنامه را اجرا کنیم، چنین خروجی حاصل میگردد و در آن دیگر Type مساوی صفر نیست:
JsonContent (Client Side): [{"Id":1,"Name":"User 1","Type":"Admin"},{"Id":2,"Name":"User 2","Type":"User"}]
تنظیمات JSON سمت کلاینت Web API
اکنون در سمت کلاینت قصد داریم اطلاعات یک کاربر را با فرمت JSON به سمت سرور ارسال کنیم. روش متداول آن توسط کتابخانهی HttpClient، استفاده از متد PostAsJsonAsync است:
var user = new User { Id = 1, Name = "User 1", Type = UserType.Writer }; var client = new HttpClient(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = client.PostAsJsonAsync(BaseAddress + "api/users", user).Result; Console.WriteLine("Response: {0}", response);
JsonContent (Server Side): {"Id":1,"Name":"User 1","Type":2}
var jsonMediaTypeFormatter = new JsonMediaTypeFormatter { SerializerSettings = new JsonSerializerSettings { Converters = { new StringEnumConverter() } } }; var response = client.PostAsync(BaseAddress + "api/users", user, jsonMediaTypeFormatter).Result; Console.WriteLine("Response: {0}", response);
اینبار مقدار دریافتی در سمت سرور به صورت ذیل است و در آن، Type دیگر عددی نیست:
JsonContent (Server Side): {"Id":1,"Name":"User 1","Type":"Writer"}
مثال کامل این بحث را از اینجا میتوانید دریافت کنید:
UsersController.zip
معرفی روش جدید نوشتن عبارات switch در C#8.0
فرض کنید یک enum که معرف تعدادی رنگ است را تعریف کردهایم:
public enum Rainbow { Red, Orange, Yellow, Green, Blue, Indigo, Violet }
class RGBColor { internal byte Red { get; } internal byte Green { get; } internal byte Blue { get; } internal RGBColor(byte red, byte green, byte blue) { Red = red; Green = green; Blue = blue; } public override string ToString() => $"rgb({Red}, {Green}, {Blue})"; }
internal static RGBColor FromRainbow(Rainbow rainbowBolor) { switch (rainbowBolor) { case Rainbow.Red: return new RGBColor(0xFF, 0x00, 0x00); case Rainbow.Orange: return new RGBColor(0xFF, 0x7F, 0x00); case Rainbow.Yellow: return new RGBColor(0xFF, 0xFF, 0x00); case Rainbow.Green: return new RGBColor(0x00, 0xFF, 0x00); case Rainbow.Blue: return new RGBColor(0x00, 0x00, 0xFF); case Rainbow.Indigo: return new RGBColor(0x4B, 0x00, 0x82); case Rainbow.Violet: return new RGBColor(0x94, 0x00, 0xD3); default: throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowBolor)); }; }
internal static RGBColor TasteTheRainbow(Rainbow rainbowColor) => rainbowColor switch { Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00), Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00), Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00), Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00), Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF), Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82), Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3), _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(rainbowColor)), };
- در ادامه تمام caseها حذف میشوند و بجای آنها صرفا مقادیر مدنظر باقی میماند. در اینجا <= به صورت expressed as خوانده میشود.
- caseهای مختلف با کاما از هم جدا میشوند.
- همچنین در سطر آخر آن نیز از یک discard استفاده شدهاست که معادل همان حالت default یا حالتی است که هیچ تطابقی صورت نگرفته باشد.
- به علاوه اگر دقت کنید، نتیجهی نهایی این switch جدید، به صورت یک مقدار، توسط متد TasteTheRainbow، بازگشت داده شدهاست. بنابراین نوشتن یک چنین عباراتی در C# 8.0، مجاز است:
var operation = "+"; int a = 1, b = 2; var result = operation switch { "+" => a + b, "-" => a - b, "/" => a / b, _ => throw new NotSupportedException() };
معرفی Property Patterns در C# 8.0
کلاس زیر را درنظر بگیرید که از تعدادی خاصیت عمومی تشکیل شدهاست:
class Address { public string AddressLine1 { get; set; } public string AddressLine2 { get; set; } public string City { get; set; } public string State { get; set; } public string PostalCode { get; set; } public string CountryRegion { get; set; } }
static class PropertyPatterns { internal static decimal ComputeSalesTax( Address location, decimal salePrice) => location switch { { State: "Fars" } => salePrice * 0.06m, { State: "Tehran", City: "Tehran" } => salePrice * 0.056m, // Other cases removed for brevity... _ => 0M }; }
معرفی Tuple Patterns در C# 8.0
در switchهای C# 8.0، میتوان از tuples نیز برای تشکیل قسمت case و همچنین مقداری که قرار است switch بر روی آن صورت گیرد، استفاده کرد:
static class TuplePatterns { internal static string RockPaperScissors( string first, string second) => (first, second) switch { ("rock", "paper") => "Rock is covered by Paper. Paper wins!", ("rock", "scissors") => "Rock breaks Scissors. Rock wins!", ("paper", "rock") => "Paper covers Rock. Paper wins!", ("paper", "scissors") => "Paper is cut by Scissors. Scissors wins!", ("scissors", "rock") => "Scissors is broken by Rock. Rock wins!", ("scissors", "paper") => "Scissors cuts Paper. Scissors wins!", (_, _) => "tie" }; }
بهبودهای Pattern Matching بر روی اشیاء در C# 8.0
فرض کنید شیء پایهی Shape را تعریف و بر اساس آن دو شیء جدید دایره و مستطیل را ایجاد کردهایم:
class Shape { protected internal double Height { get; } protected internal double Length { get; } protected Shape(double height = 0, double length = 0) { Height = height; Length = length; } } class Circle : Shape { internal double Radius => Height / 2; internal double Diameter => Radius * 2; internal double Circumference => 2 * Math.PI * Radius; internal Circle(double height = 10, double length = 10) : base(height, length) { } } class Rectangle : Shape { internal bool IsSquare => Height == Length; internal Rectangle(double height = 10, double length = 10) : base(height, length) { } }
static class ObjectPatterns { internal static string ShapeDetails(this Shape shape) => shape switch { Circle c => $"circle with (C): {c.Circumference}", Rectangle s when s.IsSquare => $"L:{s.Length} H:{s.Height}, square", Rectangle r => $"L:{r.Length} H:{r.Height}, rectangle", _ => "Unknown shape!" // Discard }; }
معرفی Positional Patterns در C# 8.0
در اینجا یک Point را داریم که میخواهیم بر اساس آن یک Quadrant را استخراج کنیم:
class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } enum Quadrant { Unknown, Origin, One, Two, Three, Four, OnBorder }
static class PositionalPatterns { internal static Quadrant AsQuadrant(Point point) => point switch { (0, 0) => Quadrant.Origin, var (x, y) when x > 0 && y > 0 => Quadrant.One, var (x, y) when x < 0 && y > 0 => Quadrant.Two, var (x, y) when x < 0 && y < 0 => Quadrant.Three, var (x, y) when x > 0 && y < 0 => Quadrant.Four, (_, _) => Quadrant.OnBorder, // Either are 0, but not both _ => Quadrant.Unknown }; }
در اینجا اگر دقت کنید و case مخصوص discards معرفی شدهاست. اولی برای حالتهایی است که هیچکدام از شرایط پیش از آن را برآورده نمیکند، مانند حالت (1,0)، در غیراینصورت سطر بعد از آن بازگشت داده میشود.
اصول نمایش Tooltip در Bootstrap
Tooltip یکی دیگر از کامپوننتهای جاوا اسکریپتی Bootstrap است.
<a rel="tooltip" title="یک سری توضیحات در اینجا" href="#">اطلاعات</a> <script type="text/javascript"> $(document).ready(function () { $("[rel='tooltip']").tooltip({placement:'top', trigger : 'hover'}); }); </script>
برای المان مورد نظر، یک title تعریف کردهایم. برای اینکه tooltip پیش فرض مرورگر در این حالت ظاهر نشود، یک rel=tooltip را در اینجا به المان اضافه کرده و سپس افزونه tooltip بوت استرپ، بر روی کلیه المانهایی با rel=tooltip فراخوانی شده است.
placement، محل قرارگیری tooltip را مشخص میکند؛ مانند بالا، پایین، چپ و راست. trigger مشخص میکند که این tooltip در چه زمانی ظاهر شود. برای مثال در حالت hover یا در حالت focus یک المان.
در اینجا بجای title، از ویژگی data-original-title نیز میتوان استفاده کرد.
تبدیل خطاهای اعتبارسنجی ASP.NET MVC به Tooltip
هدف ما در اینجا، نهایتا رسیدن به شکل زیر میباشد:
همانطور که ملاحظه میکنید، اینبار بجای نمایش خطاها در یک برچسب مقابل کنترل متناظر، این خطا صرفا در حالت فوکوس کنترل، به شکل یک Tooltip در بالای آن ظاهر شده است.
کدهای کامل View برنامه
@model Mvc4TwitterBootStrapTest.Models.User @{ ViewBag.Title = "تعریف کاربر جدید"; } @using (Html.BeginForm()) { @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) <fieldset class="form-horizontal"> <legend>تعریف کاربر جدید</legend> <div class="control-group"> @Html.LabelFor(model => model.Name, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.Name) @*@Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })*@ </div> </div> <div class="control-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.LastName) @*@Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })*@ </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"> ارسال</button> </div> </fieldset> } @section JavaScript { <script type="text/javascript"> $.validator.setDefaults({ showErrors: function (errorMap, errorList) { this.defaultShowErrors(); //اگر المانی معتبر است نیاز به نمایش تولتیپ ندارد $("." + this.settings.validClass).tooltip("destroy"); //افزودن تولتیپها for (var i = 0; i < errorList.length; i++) { var error = errorList[i]; $(error.element).tooltip({ trigger: "focus" }) // فقط در حالت فوکوس نمایش داده شود .attr("data-original-title", error.message); } }, // همانند قبل برای رنگی کردن کل ردیف در صورت عدم اعتبار سنجی و برعکس highlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).addClass(errorClass).removeClass(validClass); } else { $(element).addClass(errorClass).removeClass(validClass); $(element).closest('.control-group').removeClass('success').addClass('error'); } $(element).trigger('highlited'); }, unhighlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).removeClass(errorClass).addClass(validClass); } else { $(element).removeClass(errorClass).addClass(validClass); $(element).closest('.control-group').removeClass('error').addClass('success'); } $(element).trigger('unhighlited'); } }); //برای حالت پست بک از سرور عمل میکند $(function () { $('form').each(function () { $(this).find('div.control-group').each(function () { if ($(this).find('span.field-validation-error').length > 0) { $(this).addClass('error'); } }); }); }); </script> }
توضیحات
- با توجه به اینکه دیگر نمیخواهیم خطاها به صورت برچسب در مقابل کنترلها نمایش داده شوند، کلیه Html.ValidationMessageFor به صورت کامنت درآورده شدهاند.
- تغییر دوم مطلب جاری، اضافه شدن متد showErrors به تنظیمات پیش فرض jQuery Validator است. در این متد، اگر المانی معتبر بود، Tooltip آن حذف میشود؛ یا در سایر حالات، المانهایی که نیاز به اعتبارسنجی سمت کلاینت دارند، یافت شده و سپس ویژگی data-original-title با مقداری معادل خطای اعتبارسنجی متناظر، به این المان افزوده شده و سپس متد tooltip بوت استرپ بر روی آن فراخوانی میگردد.
به عبارتی زمانیکه یک input box در ASP.NET MVC به همراه مقادیر مرتبط با اعتبارسنجی آن رندر میشود، چنین شکلی را خواهد داشت:
<input class="text-box single-line" data-val="true" data-val-required="لطفا نام را تکمیل کنید" id="Name" name="Name" type="text" value="" />
<input class="text-box single-line input-validation-error" data-val="true" data-val-required="لطفا نام را تکمیل کنید" id="Name" name="Name" type="text" value="" data-original-title="لطفا نام را تکمیل کنید" title="">
»اولین راه حلی که به ذهن میرسد این است که پارامترهای مشخص شده را در متدهای سرویسهای مورد نظر قرار داد و به نوعی تمام سرویسها را به روز رسانی کرد. این روش به طور قطع در خیلی از قسمتهای پروژه به صورت مستقیم اثرگذار خواهد بود و در صورت نبود ابزارهای تست ممکن است با مشکلات جدی روبرو شوید.
»راه حل دوم این است که یک Message Header سفارشی بسازیم و در هر درخواست اطلاعات مورد نظر را در هدر قرار داده و سمت سرور این اطلاعات را به دست آوریم. این روش کمترین تغییر مورد نظر را برای پروژه دربر خواهد داشت و از طرفی نیاز متدهای سرویس به پارامتر را از بین میبرد و دیگر نیازی نیست تا تمام متدهای سرویسها دارای پارامترهای یکسان باشند.
پیاده سازی
برای شروع کلاس مورد نظر برای ارسال اطلاعات را به صورت زیر خواهیم ساخت:
[DataContract] public class ApplicationContext { [DataMember( IsRequired = true )] public string UserId { get { return _userId; } set { _userId = value; } } private string _userId; [DataMember( IsRequired = true )] public static ApplicationContext Current { get { return _current; } private set { _current = value; } } private static ApplicationContext _current;
public static void Register( ApplicationContext appContext ) { Current = appContext; IsRegistered = true; } }
public class ClientMessageHeaderInspector<T> : IClientMessageInspector { private readonly T _vaccine; public ClientMessageHeaderInspector( T vaccine ) { this._vaccine = vaccine; } public void AfterReceiveReply( ref Message reply, object correlationState ) { } public object BeforeSendRequest( ref Message request, IClientChannel channel ) { MessageHeader messageHeader = MessageHeader.CreateHeader( typeof( T ).Name, typeof( T ).Namespace, this._vaccine ); request.Headers.Add( messageHeader ); return null; } }
public class ApplicationContextMessageBehavior : IEndpointBehavior { ClientMessageHeaderInspector<ApplicationContext> inspector = null; public ApplicationContextMessageBehavior() { inspector = new ClientMessageHeaderInspector<ApplicationContext>( ApplicationContext.Current ); } public void AddBindingParameters( ServiceEndpoint endpoint, BindingParameterCollection bindingParameters ) { } public void ApplyClientBehavior( ServiceEndpoint endpoint, ClientRuntime clientRuntime ) { clientRuntime.MessageInspectors.Add( inspector ); } public void ApplyDispatchBehavior( ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher ) { } public void Validate( ServiceEndpoint endpoint ) { } }
در مرحله آخر باید تنظیمات مربوط به ChannelFactory را انجام دهیم.
public class ServiceMapper<TChannel> { internal static EndpointAddress EPAddress { get { return _epAddress; } } private static EndpointAddress _epAddress; public static TChannel CreateChannel( Binding binding, string uriBase, string serviceName, bool setCredential ) { _epAddress = new EndpointAddress( String.Format( "{0}{1}", uriBase, serviceName ) ); var factory = new ChannelFactory<TChannel>( binding, _epAddress ); ApplicationContext.Register( new ApplicationContext { UserId = Guid.NewGuid() } );
factory.Endpoint.Behaviors.Add( new ApplicationContextMessageBehavior() ); TChannel proxy = factory.CreateChannel(); if ( factory.Endpoint.Behaviors.OfType<ApplicationContextMessageBehavior>().Any() ) { using ( var scope = new OperationContextScope( ( IClientChannel )proxy ) ) { OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, ApplicationContext.Current ) ); } } return proxy; }
»در متد CreateChannel، ابتدا تنظیمات مربوط به EndPointAddress و ChannelFactory انجام میشود. سپس یک نمونه از کلاس ApplicationContext را توسط متد Register به کلاس مورد نظر رجیستر میکنیم. به این ترتیب مقدار خاصیت Current در کلاس ApplicationContext برابر با نمونه ساخته شده میشود. سپس کلاس ApplicationContextMessageBehavior به خاصیت Behavior در ChannelFactory اضافه میشود. در انتها نیز هدر سفارشی ساخته شده به MessageHeaderهای نمونه جاری OperationContext اضافه میشود. این عمل توسط کد زیر انجام میگیرد:
OperationContext.Current.OutgoingMessageHeaders.Add( MessageHeader.CreateHeader( typeof( ApplicationContext ).Name, typeof( ApplicationContext ).Namespace, AppConfiguration.Application ) );
استفاده از هدر سفارشی سمت سرور
حال قصد داریم که اطلاعات مورد نظر را از هدر درخواست در سمت سرور به دست آورده و از آن در کوئریهای خود استفاده نماییم. کد زیر این کار را برای ما انجام میدهد:
if ( OperationContext.Current != null && OperationContext.Current.IncomingMessageHeaders.FindHeader( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace ) > 0 ) { _application = OperationContext.Current.IncomingMessageHeaders.GetHeader<ApplicationContext>( typeof( ApplicationContext ).Name , typeof( ApplicationContext ).Namespace ); }
بررسی ساختار کامپوننت Pagination
شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import Pagination from "./common/pagination";
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} />
state = { movies: getMovies(), pageSize: 4 };
handlePageChange = page => { console.log("handlePageChange", page); };
نمایش شماره صفحات گرید، در کامپوننت صفحه بندی
برای رندر کامپوننت صفحه بندی، از کلاسهای مخصوص اینکار که در بوت استرپ تعریف شدهاند، استفاده میکنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل میشود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحهای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آنها استفاده خواهیم کرد:
import React, { Component } from "react"; class Pagination extends Component { render() { return ( <nav> <ul className="pagination"> <li className="page-item"> <a className="page-link">1</a> </li> </ul> </nav> ); } } export default Pagination;
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلمها مشاهده خواهید کرد:
اکنون باید رندر این liها را بر اساس ورودیهای این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایهای داریم که بر اساس این ورودیها، شمارهی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component { // ... getPageNumbersArray() { const { itemsCount, pageSize } = this.props; const pagesCount = Math.ceil(itemsCount / pageSize); if (pagesCount === 1) { return null; } const pages = new Array(); for (let i = 1; i <= pagesCount; i++) { pages.push(i); } return pages; } }
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات میکنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کردهایم:
class Pagination extends Component { render() { const pages = this.getPageNumbersArray(); if (!pages) { return null; } return ( <nav> <ul className="pagination"> {pages.map(page => ( <li key={page} className="page-item"> <a className="page-link">{page}</a> </li> ))} </ul> </nav> ); }
پس از ذخیرهی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شمارهی صفحات رندر شده، در پایین جدول مشخص هستند:
با داشتن 9 فیلم در آرایهی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شدهاست. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.
مدیریت انتخاب شمارههای صفحات
در این قسمت میخواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر میدهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a onClick={() => this.props.onPageChange(page)} className="page-link" style={{ cursor: "pointer" }} > {page} </a>
تا اینجا اگر برنامه را آزمایش کنیم، با کلیک بر روی لینکهای شماره صفحات، شماره صفحهی انتخابی، در کنسول توسعه دهندگان مرورگر لاگ میشود.
اکنون میخواهیم اگر صفحهای انتخاب شد، شمارهی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحهی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه میکنیم:
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} />
class Movies extends Component { state = { movies: getMovies(), pageSize: 4, currentPage: 1 };
handlePageChange = page => { console.log("handlePageChange", page); this.setState({currentPage: page}); };
پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال میکنیم:
<li key={page} className={ page === this.props.currentPage ? "page-item active" : "page-item" } >
در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان میدهد.
نمایش صفحه بندی شدهی اطلاعات
تا اینجا لیستی که نمایش داده میشود، حاوی تمام اطلاعات آرایهی this.state.movies است و بر اساس شمارهی صفحهی انتخابی، تغییر نمیکند. به همین جهت با استفاده از متد slice، تکهای از آرایهی movies را که بر اساس شماره صفحهی انتخابی و تعداد ردیفها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت میدهیم:
paginate() { const first = (this.state.currentPage - 1) * this.state.pageSize; const last = first + this.state.pageSize; return this.state.movies.slice(first, last); }
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const movies = this.paginate();
پس از ذخیرهی تغییرات و بارگذاری مجدد برنامه، اکنون میتوان نمایش صفحه بندی شدهی اطلاعات را شاهد بود:
بررسی صحت نوع پارامترهای ارسالی به کامپوننتها
تا اینجا فرض بر این است که مصرف کنندهی کامپوننت صفحه بندی، دقیقا همان ویژگیهایی را که ما طراحی کردهایم، با همان نامها و همان نوعها را حتما به آن ارسال میکند. همچنین اگر افزونهی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگیهای زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha
به ازای هر خاصیت props استفاده شدهی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:
که عنوان میکند props validation این خاصیت استفاده شده، فراموش شدهاست.
در نگارشهای قبلی React، امکانات بررسی نوعهای ارسالی به کامپوننتها، جزئی از بستهی اصلی آن بود؛ اما از نگارش 15 به بعد، به بستهی مستقلی منتقل شدهاست که باید به صورت جداگانهای در ریشهی پروژه نصب شود:
> npm i prop-types --save
البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بستهی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژولهای برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل میشود:
C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index
اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه میکنیم:
import PropTypes from "prop-types";
Pagination.propTypes = { itemsCount: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired }; export default Pagination;
سپس مقدار این خاصیت جدید را به شیءای تنظیم میکنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شدهی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import میشود، کار تعریف نوع این خواص و اجباری بودن آنها را میتوان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف میشوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشتهی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان میکند itemsCount یک عدد را میپذیرد و نوع ورودی آن اشتباه است:
البته این خطا فقط در حالت development مشاهده میشود و در حالت توزیع برنامه، خیر.
بنابراین تعریف propTypes یک best practice ایجاد کامپوننتهای React است که نه فقط بررسی نوعها را فعال میکند، بلکه میتواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آنها، اطلاعات کاملی را ارائه کند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-11.zip
DocerMaster : OS : CentOS7 IP: 192.168.64.3 DockerWorker: OS: CentOS7 IP: 192.168.64.4
sudo yum install docker
$ sudo service docker start $ sudo systemctl start docker.service
firewall-cmd --permanent --add-port=2376/tcp firewall-cmd --permanent --add-port=2377/tcp firewall-cmd --permanent --add-port=7946/tcp firewall-cmd --permanent --add-port=7946/udp firewall-cmd --permanent --add-port=4789/udp firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload
systemctl restart docker
~]# firewall-cmd --permanent --add-port=2376/tcp ~]# firewall-cmd --permanent --add-port=7946/tcp ~]# firewall-cmd --permanent --add-port=7946/udp ~]# firewall-cmd --permanent --add-port=4789/udp ~]# firewall-cmd --permanent --add-port=80/tcp ~]# firewall-cmd --reload ~]# systemctl restart docker
sudo docker swarm init –advertise-addr 192.168.64.3
همانطور که مشاهده میکنید، پس از راه اندازی، اعلانی مبنی بر اینکه این نود به عنوان Manager شناخته شده و اینکه برای اضافه کردن یک نود Worker چه دستوری را باید اجرا کرد، نمایش داده شدهاست.
اکنون کافیاست این خط کد را در نود Worker کپی کنیم:
بعد از موفقیت آمیز بودن اجرای آن، میتوانید در کامپیوتر Master، با دستور زیر تمام نودها را مشاهده کنید:
$ sudo docker node ls
همانطور که مشاهده میکنید، دو نود وجود دارد که یکی به عنوان Leader شناخته میشود. هر زمانی که نیاز بود، میشود به راحتی یک Worker دیگر را اضافه کرد.
برای راه اندازی یک کانتینر، swarm از CLI کاملی برخوردار هست؛ اما مایلم اینجا از یک ابزار خوب، برای مدیریت Swarm استفاده کنم. Portainer به عنوان یه ابزار عالی برای مدیریت Imageها و Containerهای داکر محسوب میشود که کاملا swarm را پشتیبانی میکند.
برای راه اندازی portainer کافی است کد زیر را در سیستم Master اجرا کنید:
$ docker volume create portainer_data $ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer
البته به دلیل عدم دسترسی به داکر هاب از کشور ایران، عملا امکان pull کردن این image، مستقیما از داکر هاب و بدون وی پی ان وجود ندارد.
بعد از موفقیت آمیز بودن راه اندازی portainer میتوانید از طریق آدرس http://192.168.64.3:9000 به آن دسترسی داشته باشید. در اولین ورود، پسورد ادمین را تنظیم میکنید و بعد از وارد شدن، صفحهای مطابق شکل زیر را خواهید دید:
اگر بر روی منوی swarm کلیک کنید، همهی نودها را مشاهده خواهید کرد و در صورتیکه بر روی Containers کلیک کنید، همهی Container هایی را که بر روی این سرور وجود دارند، خواهید دید. مهمترین قسمت، بخش Service هاست که مشخصات Container هایی که روی swarm توزیع شدن را نشان میدهد و همینطور تعداد Container هایی از این image که Scale شدند. همینطور که میبینید فعلا فقط همین Portainer در حال اجراست.
اجازه دهید یک مثال کاربردیتر بزنیم و یک سرویس را ایجاد کنیم.
من بر روی کامپیوتر شخصیام و نه سرورها، با دستور زیر یک پروژهی MVC را با دات نت Core ایجاد میکنم:
dotnet new mvc
و سپس دستور dotnet publish را اجرا میکنم و به پوشهای که محتویات پابلیش شده در آن قرار دارند رفته و یک فایل بدون پسوند را به نام dockerfile ایجاد میکنم و متن زیر را در آن مینویسم:
همینطور که میبینید من از image مخصوص اجرای دات نت Core در این container استفاده میکنم. پوشهی کانتینر را تنظیم میکنم و همهی فایلهایی که در پوشهی جاری سیستم خودم وجود دارند را به پوشهی جاری کانتینر منتقل میکنم و سپس دستور دات نت را با پارامتر اسم dll پروژهام اجرا میکنم. این کل محتویات فایل داکر من هست.
ترمینال را در همین پوشهی publish باز میکنم و دستور زیر را اجرا میکنم:
docker build –t swarmtest:dev .
docker save swarmtest:dev –o swarmtest.tar
طبق شکل زیر یک فایل tar که حاوی image برنامه من هست، ایجاد شد:
حالا با دستور زیر این فایل رو به سرور Master منتقل میکنم:
scp –r swarmtest.tar root@192.168.64.3:/srv/images
همانطور که میبینید، فایل tar به پوشهای که قبلا در سرور ایجاد کردم، منتقل شد.
حالا به سرور و پوشهای که فایل tar آنجا قرار دارد رفته و با دستور زیر این image را بر روی سیستم load میکنم:
sudo docker load –i swarmtest.tar
همانطور که در تصویر میبینید، بعد از load شدن، image مورد نظرمان به داکر اضافه شدهاست.
حالا برای اجرا کردن این سرویس بر روی swarm، آدرس portainer را باز میکنیم و به قسمت services میرویم و بر روی دکمهی add service کلیک میکنیم:
در قسمت نام، نام سرویس و در قسمت imageConfiguration از منوی imageها، ایمیجی را که ایجاد کردیم، انتخاب میکنیم. در قسمت Replicas تعداد instanceهای container ای را که میخواهیم از روی image ایجاد شوند، مشخص میکنیم. (این قسمت را بر روی هر وضعیتی میتوانیم قرار دهیم و زیاد و کم کنیم) و در قسمت port mapping، پورت درون Container و پورتی را که میخواهیم بر روی هاست به نمایش درآید، وارد میکنیم.
همانطور که میبینید من به راحتی میتوانم تعداد Containerها را Scale کنم و نگرانیای بابت load balancing و اینکه کدام container بر روی کدوم سرور ایجاد میشود، نخواهم داشت.
برای نمایش برنامه کافی است پورتی را که برای هاست وارد کردیم، با آی پی Master وارد کنیم: