نظرات مطالب
پیاده سازی CQRS توسط MediatR - قسمت دوم
روش ارسال اطلاعات با فرمت JSON (و نه form-urlencoded) به سرور در قسمت «ارسال درخواست‌هایی از نوع POST » مطلب «آزمایش Web APIs توسط Postman - قسمت اول - معرفی» بحث شده‌است « ... اما اگر برای مثال نیاز باشد تا اطلاعات را با فرمت JSON، به سمت Web API ای ارسال کنیم، نیاز است گزینه‌ی raw را انتخاب کرد و سپس قالب پیش‌فرض آن‌را که text است به JSON تغییر داد ... ».
مطالب
Portable Class Library چیست و چگونه از آن استفاده کنیم؟
Portable Class Library چیست؟
Portable Class Library برای تولید Assembly مدیریت شده که قابیلت استفاده در اکثر تکنولوژی‌ها نظیر (WP7(Windows Phone 7 و WPF و Silverlight و Windows Store App و حتی XBox را داراست استفاده می‌شود.  این نوع پروژه‌ها میزان استفاده مجدد از کد‌ها رو به حداکثر ممکن می‌رسونه و در عوض تعداد پروژه‌های مورد نیاز رو به حداقل. مورد اصلی استفاده از اون‌ها برای تولید Multi-Targeted Application هاست. برای مثال در تولید پروژه‌های تحت Windows که با تکنولوژی WPF پیاده سازی می‌شوند پروژه ViewModel و  Model رو با این تکنولوژی پیاده سازی می‌کنند تا در آینده اگر نیاز بود قسمتی از پروژه با استفاده از Silverlight یا Windows Store App انجام بشه بتونیم Model , ViewModel نوشته شده رو به این پروژه‌ها Reference بدیم. تا قبل از برای انجام این کار باید این دو بخش رو دوباره برای هر تکنولوژی بازنویسی می‌کردیم.
روش استفاده
زمانی که یک پروژه از نوع Portable Class Library رو ایجاد می‌کنیم باید حداقل 2 یا چند تا از Platform‌های مورد نظر رو انتخاب کنیم. به صورت زیر :


بعد از انتخاب گزینه Portable Class Library و زدن کلید Enter  فرم زیر ظاهر خواهد شد که در این قسمت باید Platform‌های مورد نظر رو انتخاب کنیم.


در اینجا من 2 Platform رو انتخاب کردم. یکی Silverlight و Net 4.5. یعنی این Portable Class Library هم می‌تونه به پروژه Silverlight و هم به Class library .Net 4.5 رفرنس داده بشه.
حتما می‌پرسید چه طوری؟
در واقع Microsoft برای انجام این کار فقط یک سری از Reference‌های مشترک بین این 2 Platform رو به پروژه اضافه میکنه و همین موضوع باعث شده کار با این پروژه‌ها نیاز به یکم مهارت داشته باشه.
برای مثال اگر قصد استفاده از تکنولوژی Async&Await  رو دارید باید یکم به خودتون زحمت بدید و CTP مورد نظر رو نصب کنید.(حالا اگر از Net4 استفاده می‌کنید که باید از یک روش دیگه استفاده کنید که در یک مقاله جداگانه براتون توضیح خواهم داد).
به جدول زیر دقت کنید.
 Xbox 360  Windows Phone Silverlight 
Windows Store 
.NET Framework 
 Feature 
 
 
 √  √  
 Core 
  
 √  √  √  LINQ 
 Only 7.5 
 √  √  
 IQueryable 
   
 
 Only 4.5  Dynamic keyword 
   √  
 √  Managed Extensibility Framework (MEF) 
  √  √  
 √  Network Class Library (NCL) 
  
 √  
 √  Serialization 
  
 √  
 √  Windows Communication Foundation (WCF) 
  √  √  
  Only 4.5 
 
 Model-View-View Model (MVVM) 
   
 
 Only 4.0.3 and 4.5  Data annotations 
 √  √  
 
 Only 4.0.3 and 4.5  XLINQ 

در جدول بالا به صورت کامل مشخص شده که در هر پلاتفرم چه چیزی Support میشه. برای مثال Data Annotation فقط در Net4.3 , 4.5  قابل استفاده است.
اسمبلی‌های زیر در یک Portable Class Library  قابل استفاده هستند.
  • mscorlib.dll

  • System.dll

  • System.Core.dll

  • System.Xml.dll

  • System.ComponentModel.Composition.dll

  • System.Net.dll

  • System.Runtime.Serialization.dll

  • System.ServiceModel.dll

  • System.Xml.Serialization.dll

  • System.Windows.dll  - From Silverlight

در صورتی که از VS2010 استفاده می‌کنید می‌تونید از این جا Portable Library Tools رو دانلود کنید. بعد از نصب دقیقا همین مراحل رو برای ایجاد پروژه انجام بدید.
پیاده سازی یک مثال عملی
در این مثال قصد دارم یک کلاس برای مدیریت تاریخ شمسی درست کنم. مراحل زیر رو دنبال کنید
 یک Portable Class Library  به نام Common به پروژه اضافه کنید.
یک کلاس به نام PersianDate به برنامه اضافه کرده به صورت زیر
 public class PersianDate<TCalendar> where TCalendar : System.Globalization.Calendar
    {
        public PersianDate()
        {
            this.CurentCalendar = System.Activator.CreateInstance<TCalendar>();
        }

        public TCalendar CurentCalendar 
        {
            get;
            private set;
        }

        public string GetDate()
        {
            return string.Format( "{0}/{1}/{2}", this.CurentCalendar.GetYear( DateTime.Today ).ToString( "####0000" )
                , this.CurentCalendar.GetMonth( DateTime.Today ).ToString( "##00" )
                , this.CurentCalendar.GetDayOfMonth( DateTime.Today ).ToString( "##00" ) );
        }
    }
کلاس بالا به صورت Generic تعریف شده که شما باید هنگام استفاده حتما یک نوع Calendar رو بهش بدید. دلیل این کار اینه که کلاس PersianCalendar در Portable Class Library وجود ندارد و فقط یک کلاس پایه Calendar در فضای نام System.Globalization وجود داره.

حالا یک پروژه از نوع Console Application به برنامه اضافه کنید و یک Reference  به پروژه Common بدید و بعد کلاس زیر رو بنویسید.
public class CustomPersianDate
    {
        public CustomPersianDate()
        {

        }

        public static Common.PersianDate<System.Globalization.PersianCalendar> PersianCalendar
        {
            get
            {
                return _persianCalendar ?? ( _persianCalendar = new Common.PersianDate<System.Globalization.PersianCalendar>() );
            }
        }
        private static Common.PersianDate<System.Globalization.PersianCalendar> _persianCalendar;
    }
در این قسمت ما نوع TCalendar رو از نوع PersianCalendar تعیین کردیم.

حالا یک پروژه از نوع Silverlight به برنامه اضافه کنید و یک Reference  به پروژه Common بدید و بعد کلاس بالا بدون تغییر بنویسید.
نمای کلی پروژه باید به صورت زیر باشد.

بعد پروژه ConsoleApplication  رو به عنوان پروژه StartUp انتخاب کنید و فایل Program  رو به صورت زیر تغییر بدید.
class Program
    {
        static void Main( string[] args )
        {
            Console.WriteLine( "Today Is ?{0}", CustomPersianDate.PersianCalendar.GetDate() );

            Console.ReadLine();
        }
    }

خوب نتیجه به صورت زیر خواهد بود:

برای پروژه Silverlight هم نتیجه قطعا به همین صورت است.

همان طور که دید انتخاب نوع Calendar به خود Application‌ها واگذار شد و شما می‌تونید هر نوع تقویم رو به عنوان TCalendar تعیین کنید.

امیدوارم شما هم مثل من به این تکنولوژی دلچسب علاقه پیدا کرده باشید.





نظرات اشتراک‌ها
نسخه راستچین شده AdminLTE 2.2.1
با سلام.

واقعا ممنون بابت این اشتراک. فقط یک مورد رو من دیدم که گفتم بیان کنم.

در نسخه 40.0.3  فایرفاکس ،  sidebar-menu  بدین صورت هستش :


من فایل css شمارو به این صورت تغییر دادم :

.sidebar-menu > li > a {
  padding: 12px 15px 12px 5px;
  display: block;
  height: 50px;
}
.sidebar-menu > li > a >i,
.sidebar-menu > li > a >span:not([class*="label-primary"]){
    float: right;
    line-height: 25px;
}

مشکل برطرف شد :

  البته در کروم مشکلی نداره. البته قطعا شما با فایل css تون آشنایی بهتری دارید و میتونید راه حل‌های بهتری استفاده کنید

موفق باشید
 
مطالب
نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» مشاهده کردیم که Angular در روش فرم‌های مبتنی بر قالب‌ها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگی‌های minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی می‌کند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنج‌های سفارشی مخصوص حالت فرم‌های مبتنی بر قالب‌ها را بررسی کنیم.


تدارک مقدمات مثال این قسمت

این مثال، در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه می‌کنیم:
 >ng g m CustomValidators -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
 >ng g c CustomValidators/user-register
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف می‌کنیم:
 >ng g cl CustomValidators/user
با این محتوا:
export class User {
  constructor(
    public username: string = "",
    public email: string = "", 
    public password: string = "", 
    public confirmPassword: string = "" 
  ) {}
}
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمه‌ی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمه‌ی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.



تعریف اعتبارسنج سفارشی ایمیل‌ها

هرچند می‌توان اعتبارسنجی ایمیل‌ها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوه‌ی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه می‌دهیم:
 >ng g d CustomValidators/EmailValidator -m custom-validators.module
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام می‌دهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی می‌شود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده می‌کنید:
import { Directive } from "@angular/core";
import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms";

@Directive({
  selector:
    "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
  ]
})
export class EmailValidatorDirective implements Validator {
  validate(element: AbstractControl): { [key: string]: any } {
    const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
    const valid = emailRegex.test(element.value);
    return valid ? null : { appEmailValidator: true };
  }
}
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آن‌را همانند ویژگی‌های HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" 
name="email" [(ngModel)]="model.email">

- روش تعریف selector آن اندکی متفاوت است:
selector:
"[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شده‌است که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شده‌است:
"apps": [
{
   // ...
   "prefix": "app",
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نام‌ها توصیه شده‌است.
ب) در اینجا formControlName، formControl و ngModel قید شده‌ی در کنار نام selector این دایرکتیو را نیز مشاهده می‌کنید. وجود آن‌ها به این معنا است که کلاس این دایرکتیو، به المان‌هایی که به آن‌ها ویژگی appEmailValidator اضافه شده‌است و همچنین آن المان‌ها از یکی از سه نوع ذکر شده هستند، اعمال می‌شود و در سایر موارد بی‌اثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا می‌توان نوشت:
 selector: "[appEmailValidator]"

- پس از آن قسمت providers را مشاهده می‌کنید:
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنج‌های توکار مبتنی بر قالب‌ها) اضافه می‌کند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کننده‌های توکار Angular ضروری است.

- سپس پیاده سازی اینترفیس توکار Validator را مشاهده می‌کنید:
 export class EmailValidatorDirective implements Validator {
این اینترفیس جزو مجموعه‌ی فرم‌های مبتنی بر قالب‌ها است و از آن جهت نوشتن اعتبارسنج‌های سفارشی می‌توان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
 validate(element: AbstractControl): { [key: string]: any }
کار این متد این است که المانی را که appEmailValidator به آن اعمال شده‌است، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار می‌دهد. به این ترتیب می‌توان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
validate(element: AbstractControl): { [key: string]: any } {
  const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
  const valid = emailRegex.test(element.value);
  return valid ? null : { appEmailValidator: true };
}
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعده‌ی نوشته شده بررسی می‌شود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده می‌شود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را می‌توان بازگشت داد.

- اکنون که این دایرکتیو جدید طراحی و ثبت شده‌است (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آن‌را به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آن‌را به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email">


نحوه‌ی طراحی خروجی متد validate

هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشده‌است و همینقدر که این خروجی یک شیء key/value باشد، کفایت می‌کند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شی‌ءایی را بازگشت می‌دهد:
 { required:true }
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه می‌دهد:
{ minlength : {
     requiredLength : 3,
     actualLength : 1
   }
}
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شده‌است:
 { appEmailValidator: true }
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
<div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
The entered email is not valid.
</div>
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شده‌است، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجه‌ی نهایی، نمی‌شود. یا حتی می‌توان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شده‌است و هر چیزی را می‌پذیرد.
از دیدگاه اعتبارسنج فرم‌های مبتنی بر قالب‌ها، همینقدر که آرایه‌ی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیده‌است و اعتبارسنجی المان را شکست خورده ارزیابی می‌کند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه می‌دهد (بر اساس نیازهای نمایشی برنامه).


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

در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمه‌ی عبور و تائید کلمه‌ی عبور را مشاهده می‌کنید:
public password: string = "",
public confirmPassword: string = ""
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونه‌ی سفارشی آن‌را همانند EmailValidatorDirective فوق تهیه می‌کنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه می‌کنیم:
 >ng g d CustomValidators/EqualValidator -m custom-validators.module
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز می‌شود.

در ادامه کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
import { Directive, Attribute } from "@angular/core";
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";

@Directive({
  selector:
    "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EqualValidatorDirective,
      multi: true
    }
  ]
})
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });

    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }

    if (
      otherControl &&
      otherControl.errors &&
      selfValue === otherControl.value
    ) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
  }
}
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شده‌است تا بتوان selector آن‌را به المان‌های HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شده‌است تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شده‌است؛ اما چون در اینجا می‌خواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کننده‌ی مقدار آن‌را نیز افزوده‌ایم:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}
به این ترتیب زمانیکه قرار است فیلد کلمه‌ی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شده‌است تا کار فعال سازی ابتدایی صورت گیرد:
<input #password="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شده‌است. این compare-to همان نامی است که به Attribute@ نسبت داده شده‌است. سپس مقداری که به این ویژگی نسبت داده می‌شود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگی‌ها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند می‌توان ویژگی‌های بیشتری را به سازنده‌ی آن نسبت داد.

یک نکته: می‌توان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج می‌گردد.

- در ابتدای این اعتبارسنج، نحوه‌ی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده می‌کنید:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شده‌است.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت می‌شود، می‌توان به کنترل دوم، توسط element.root.get دسترسی یافت.

- در ادامه‌ی کار، مقایسه‌ی ساده‌ای را مشاهده می‌کنید:
    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده می‌شود.

- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفته‌است:
    if (otherControl && otherControl.errors && selfValue === otherControl.value) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال می‌شود. اگر پیشتر کلمه‌ی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمه‌ی عبور قبلی به حالت غیرمعتبر تنظیم شده‌است. اما پس از تکمیل فیلد تائید کلمه‌ی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر می‌باشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف می‌کنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شده‌است). حذف این کلید سبب نال شدن آرایه‌ی errors یک شیء نمی‌شود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه می‌کند. بنابراین در ادامه کار، setErrors یا تنظیم آرایه‌ی errors به نال هم انجام شده‌است. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.


تکمیل کامپوننت فرم ثبت نام کاربران

اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا می‌کند:
import { NgForm } from "@angular/forms";
import { User } from "./../user";
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-user-register",
  templateUrl: "./user-register.component.html",
  styleUrls: ["./user-register.component.css"]
})
export class UserRegisterComponent implements OnInit {
  model = new User();

  constructor() {}

  ngOnInit() {}

  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
در اینجا تنها کار مهمی که انجام شده‌است، ارائه‌ی خاصیت عمومی مدل، جهت استفاده‌ی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آن‌را نیز به صورت ذیل تکمیل می‌کنیم:

ابتدای فرم
<div class="container">
  <h3>Registration Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
در اینجا novalidate اضافه شده‌است تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهله‌ای از فرم دسترسی یافته و آن‌را به متد submitForm کامپوننت ارسال کرده‌ایم.

تکمیل قسمت ورود نام کاربری

    <div class="form-group" [class.has-error]="username.invalid && username.touched">
      <label class="control-label">User Name</label>
      <input #username="ngModel" required maxlength="8" minlength="4" type="text"
        class="form-control" name="username" [(ngModel)]="model.username">
      <div *ngIf="username.invalid && username.touched">
        <div class="alert alert-info">
          errors: {{ username.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.required">
          username is required.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.minlength">
          username should be minimum {{username.errors.minlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.maxlength">
          username should be max {{username.errors.maxlength.requiredLength}} characters.
        </div>
      </div>
    </div>
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.


تکمیل قسمت ورود ایمیل

    <div class="form-group" [class.has-error]="email.invalid && email.touched">
      <label class="control-label">Email</label>
      <input #email="ngModel" required appEmailValidator type="text" class="form-control"
        name="email" [(ngModel)]="model.email">
      <div *ngIf="email.invalid && email.touched">
        <div class="alert alert-info">
          errors: {{ email.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.required">
          email is required.
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
          The entered email is not valid.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی استفاده‌ی از دایرکتیو جدید appEmailValidator را ملاحظه می‌کنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجه‌ی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شده‌است.


تکمیل قسمت‌های ورود کلمه‌ی عبور و تائید آن

    <div class="form-group" [class.has-error]="password.invalid && password.touched">
      <label class="control-label">Password</label>
      <input #password="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
      <div *ngIf="password.invalid && password.touched">
        <div class="alert alert-info">
          errors: {{ password.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.required">
          password is required.
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.appValidateEqual">
          Password mismatch. Please complete the confirmPassword .
        </div>
      </div>
    </div>

    <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched">
      <label class="control-label">Retype password</label>
      <input #confirmPassword="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword">
      <div *ngIf="confirmPassword.invalid && confirmPassword.touched">
        <div class="alert alert-info">
          errors: {{ confirmPassword.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.required">
          confirmPassword is required.
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.appValidateEqual">
          Password mismatch.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آن‌را به فیلدهای کلمه‌ی عبور و تائید آن مشاهده می‌کنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شده‌است و سبب نمایش خطای اعتبارسنجی متناسبی می‌شود.


تکمیل انتهای فرم

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا بررسی می‌شود که آیا فرم معتبر است یا خیر. اگر خیر، دکمه‌ی submit آن غیرفعال می‌شود و برعکس.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
کار با Docker بر روی ویندوز - قسمت پنجم - ایجاد Imageهای سفارشی
تا اینجا با نحوه‌ی اجرای برنامه‌های مختلف توسط داکر مانند وب سرور لینوکسی nginx و یا IIS ویندوزی آشنا شدیم؛ اما هنوز محتوایی را در آن‌ها هاست نکرده‌ایم. در این قسمت این موضوع را بررسی خواهیم کرد و در طی این فرآیند، با نحوه‌ی ساخت Imageهای سفارشی نیز آشنا خواهیم شد.


روش نگاشت محتوای یک سایت استاتیک در یک Container که وب سرور است

فرض کنید یک سایت استاتیک بوت استرپی را تهیه کرده‌اید و قصد دارید آن‌را توسط وب سرور nginx، هاست کنید. برای این‌کار، چندین گزینه پیش روی ما هستند:
گزینه‌ی اول: دریافت image مربوط به nginx، سپس ایجاد یک container از آن و در آخر با استفاده از «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» که در قسمت قبل بررسی کردیم، این وب سایت را آماده‌ی اجرا و دسترسی می‌کنیم.
گزینه‌ی دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container.
گزینه‌ی سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است و در این حالت تنها کافی است این image را تبدیل به container اجرایی کنیم.


روش اول: به اشتراک گذاری فایل سیستم میزبان با کانتینر وب سرور جهت هاست آن

در قسمت قبل، یک فایل tar ایجاد شده‌ی در سیستم میزبان ویندوزی را با یک کانتینر لینوکسی به اشتراک گذاشتیم تا بتوانیم محتویات آن‌را استخراج کنیم. در اینجا قصد داریم پوشه‌ی وب سایت استاتیک خود را که در سیستم میزبان ویندوزی قرار دارد، با وب سرور nginx که توسط یک container در حال اجرا است، به اشتراک بگذاریم تا آن‌را هاست کند.
فرض کنید وب سایت استاتیک ما در مسیر c:\users\vahid\mysite سیستم میزبان قرار دارد که داخل آن یک فایل index.html و تعدادی فایل css و js آماده‌ی برای هاست شدن، وجود دارند. برای هاست آن توسط nginx، از دستور زیر استفاده خواهیم کرد:
 docker run --rm -it -p 8080:80 -v c:\users\vahid\mysite:/usr/share/nginx/html nginx
در این دستور:
- سوئیچ rm سبب می‌شود تا پس از خاتمه‌ی کار nginx، این container نیز حذف شود.
- از سوئیچ it استفاده شده‌است تا با فشردن ctrl+c، بتوانیم پروسه‌ی container را خاتمه دهیم و پس از آن، برنامه‌ی nginx دیگر در background در حال اجرا نباشد (اجرای آن در foreground).
- سپس پورت 8080 سیستم میزبان، به پورت 80 وب سرور nginx نگاشت شده‌است. چون containerها دارای network stack خاص خودشان هستند (که آن‌را در قسمت سوم بررسی کردیم)، پورت 80 آن‌ها با پورت 80 سیستم میزبان تداخل نمی‌کند و اگر برای مثال بر روی پورت 80 سیستم جاری، IIS در حال اجرا باشد، سبب عدم اجرا شدن وب سرور nginx به دلیل تداخل پورت‌ها نمی‌شود.
- در ادامه روش volume mount را مشاهده می‌کنید که در قسمت قبل بررسی کردیم. مسیر c:\users\vahid\mysite سیستم میزبان، به مسیر ویژه‌ی /usr/share/nginx/html داخل container نگاشت شده‌است. این مسیر، یک مسیر استاندارد بوده و در مستندات docker hub این وب سرور، ذکر شده‌است.
- در آخر هم نام image این وب سرور را ذکر کرده‌ایم.

پس از اجرای این دستور، اگر nginx پیش‌تر دریافت نشده باشد، image آن دریافت شده، یک container بر اساس آن ساخته می‌شود و سپس با پارامترهایی که توضیح دادیم، اجرا خواهد شد. اکنون اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد.


روش دوم: کپی کردن فایل‌های وب سایت از سیستم میزبان، به درون فایل سیستم خود container

همانطور که در قسمت سوم نیز بررسی کردیم، فایل سیستم مربوط به هاست، به طور کامل از فایل سیستم container، جدا و ایزوله است و بدون volume mount، یک container نمی‌تواند به فایل‌های میزبان خود دسترسی پیدا کند. بنابراین گزینه‌ی دیگری که در اینجا وجود خواهد داشت، کپی کردن فایل‌های میزبان و انتقال آن‌ها به container می‌باشد؛ شبیه به کپی کردن فایل‌ها از یک کامپیوتر موجود در شبکه به کامپیوتر دیگری در آن.
برای این منظور ابتدا nginx را در پس‌زمینه اجرا می‌کنیم:
 docker run -d -p 8080:80 --name nginx nginx
در این دستور، سوئیچ‌های rm و it حذف شده‌اند. علت اینجا است که سوئیچ d، سبب اجرای این دستور در پس‌زمینه می‌شود؛ یعنی بلافاصله سبب بازگشت ما به خط فرمان خواهد شد و در این حالت نمی‌خواهیم که این container حذف شود. همچنین یک نام نیز به آن انتساب داده شده‌است تا بتوان ساده‌تر با آن کار کرد.
پس از اجرای این دستور و بازگشت به command prompt، جهت اطمینان حاصل کردن از اجرای آن در پس زمینه، دستور docker ps را صادر می‌کنیم که لیست آن، حاوی گزارشی از container‌های در حال اجرا است.
اکنون توسط دستور ویژه‌ی docker exec، می‌خواهیم درون یک container در حال اجرا، پروسه‌ای را اجرا کنیم. یعنی با اینکه پروسه‌ی nginx داخل این container در حال اجرا است، برای مثال می‌خواهیم یک shell را نیز داخل آن اجرا کنیم:
 docker exec -it nginx bash
در اینجا دستور docker exec، سبب اجرای bash shell داخل کانتینری با نام nginx می‌شود (همان سوئیچ name در دستور قبلی و نه نام image آن) و چون می‌خواهیم به این shell در foreground دسترسی داشته باشیم، از سوئیچ it نیز استفاده شده‌است. پس از اجرا شدن bash shell، اکنون به فایل سیستم این container دسترسی یافته‌ایم. برای مثال دستور ls را صادر کنید تا لیستی از آن‌را مشاهده نمائید. سپس به کمک آن، به پوشه‌ی ویژه‌ی html این وب سرور وارد می‌شویم:
 cd /usr/share/nginx/html
و برای مثال می‌توان در آن تغییر ایجاد کرد:
ls
mv index.html index2.html
exit
این دستورات سبب می‌شوند تا فایل پیش‌فرض index.html آن، به index2.html تغییر نام یابد و سپس از این shell خارج می‌شویم و به shell سیستم میزبان باز خواهیم گشت. در اینجا دستور docker cp (که در PowerShell سیستم میزبان اجرا می‌شود)، امکان کپی کردن فایل‌ها را از سیستم میزبان به یک container میسر می‌کند.
 docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
پس از دستور docker cp ابتدا مسیر مبداء مشخص می‌شود و سپس ابتدا نام container مقصد به همراه یک : و در ادامه مسیر مقصد نهایی کپی در آن container ذکر خواهند شد. به این ترتیب فایل‌های وب سایت استاتیک ما در سیستم میزبان به پوشه‌ی html مخصوص nginx، در کانیتنری که در حال اجرای آن است کپی می‌شوند. برای آزمایش صحت این کپی می‌توان دستور زیر را صادر کرد که لیست فایل‌های این پوشه‌ی html را نمایش می‌دهد:
 docker exec nginx ls /usr/share/nginx/html
اینبار نیز اگر در سیستم میزبان، مسیر http://localhost:8080 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد که فایل‌های آن از داخل خود container تامین می‌شوند و وابستگی به سیستم میزبان ندارند.


روش سوم: ایجاد یک image سفارشی که از ابتدا به همراه فایل‌های وب سایت استاتیک ما است

در روش دوم، موفق شدیم که فایل‌های مدنظر خود را به درون container در حال اجرا کپی کنیم. اکنون می‌خواهیم یک snapshot را از آن تهیه کنیم؛ شبیه به کاری که با ماشین‌های مجازی نیز انجام می‌شود و این روشی است که از آن برای ساخت یک image سفارشی استفاده می‌شود. برای این منظور از دستور docker commit استفاده می‌شود تا تصویری را از وضعیت یک container در حال اجرا، در آن لحظه تهیه کنیم:
 docker commit nginx mysite:nginx
پس از دستور docker commit، نام container ای که می‌خواهیم تصویر وضعیت جاری آن‌را ذخیره کنیم، ذکر می‌شود. پس از آن به صورت اختیاری می‌توان یک نام جدید و همچنین tag ای را برای آن ذکر کرد.
اکنون پس از اجرای این دستور، با استفاده از فرمان docker images می‌توان مشاهده کرد که image جدید mysite، با tag ای معادل nginx، ایجاد شده‌است.
در ادامه برای اجرای این image جدید، می‌توان از دستور زیر استفاده کرد:
 docker run -d -p 8090:80 --name mysite mysite:nginx
روش اجرای آن همانند سایر imageهای موجود است و در اینجا از نام image به همراه tag آن استفاده شده‌است. همچنین پورت نگاشت شده‌ی آن‌را به سیستم میزبان نیز 8090 انتخاب کرده‌ایم. نامی را نیز به آن نسبت داده‌ایم تا بتوان از آن در دستور docker exec استفاده کرد.
اکنون اگر در سیستم میزبان، مسیر http://localhost:8090 را در مرورگر باز کنید، وب سایت استاتیک خود را مشاهده خواهید کرد و یا توسط دستور زیر می‌توانید فایل‌های موجود در پوشه‌ی html وب سرور nginx این container جدید در حال اجرا را ملاحظه نمائید:
 docker exec mysite ls /usr/share/nginx/html
که این فایل‌ها نه از طریق نگاشت فایل سیستم میزبان، به مسیری در container جاری تامین شده‌اند و نه از جائی به داخل آن کپی شده‌اند. بلکه دقیقا از image از پیش آماده شده‌ی آن خوانده شده‌اند.


نگاهی به لایه‌های یک Image در مقایسه با یک Container

زمانیکه خواستیم image جدید و سفارشی خاص خود را ایجاد کنیم، با image اصلی nginx شروع کردیم. اولین لایه‌ی موجود در این image، سیستم عاملی است که می‌تواند آن‌را اجرا کند. برفراز این لایه، لایه‌ی خود nginx قرار گرفته‌است. اگر خواستید تاریخچه‌ی ایجاد یک image را مشاهده کنید، از دستور docker history nginx استفاده نمائید. خروجی آن لیست دستوراتی را نمایش می‌دهد که برای ساخت این image مورد استفاده قرار گرفته‌اند. البته دستور docker history nginx --no-trunc، اطلاعات بیشتری را با نمایش لیست کامل و خلاصه نشده‌ی دستورات، ارائه می‌دهد. این دستورات را در صفحه‌ی docker hub هر image نیز می‌توان مشاهده کرد. در قسمت full description هر image، در ابتدای توضیحات، قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک فایل که دقیقا همین دستورات را لیست کرده‌است. به این فایل، docker file گفته می‌شود که روش ساخت یک image را توضیح می‌دهد. هدف آن، خودکار سازی اجرای دستوراتی است که سبب ساخت یک image می‌شوند.

در ادامه اگر از این image، یک container را ایجاد کنیم، این container هر دو لایه‌ی OS و Framework را به همراه خواهد داشت؛ به علاوه‌ی لایه‌ی دیگری به نام Container/Run که می‌توان فایل‌های آن‌را خواند و یا در آن نوشت. بنابراین لایه‌ای که فایل‌های وب سایت استاتیک ما در آن کپی شدند، دقیقا همین لایه‌است.


و زمانیکه از یک container تصویری تهیه می‌شود، تغییراتی را که به فایل سیستم آن ایجاد کرده‌ایم، به صورت یک لایه‌ی جدید بر روی لایه‌های قبلی آن image، ظاهر و ثبت می‌شود. برای اثبات این موضوع، می‌توان از دستور docker diff nginx استفاده کرد. در اینجا nginx نام container ای است که می‌خواهیم تغییرات آن‌را با image قبلی که بر پایه‌ی آن ایجاد شده‌است، مشاهده کنیم.


تبدیل دستورات docker به یک docker file

تا اینجا یک چنین دستوراتی را برای اجرای کانتینر nginx، کپی فایل‌ها به آن و سپس تهیه‌ی یک تصویر از آن، اجرا کردیم:
docker run -d -p 8080:80 --name nginx nginx
docker cp c:\users\vahid\mysite nginx:/usr/share/nginx/html
docker commit nginx mysite:nginx
برای خودکار سازی آن‌ها هرچند می‌توان این دستورات را در یک اسکریپت نیز قرار داد، اما docker، قابلیت پردازش اسکریپت‌های خاص خود را نیز دارد که به آن Dockerfile گفته می‌شود. برای این منظور سطرهای فوق به صورت زیر تغییر می‌کنند:
بجای سطر اول، تنها نام image ای را که می‌خواهیم کار را بر مبنای آن انجام دهیم، ذکر می‌کنیم:
 FROM nginx
دستور دوم نیز تبدیل به دستور کپی Docker می‌شود:
 COPY mysite /usr/share/nginx/html
این دو سطر را به صورت یک فایل متنی، با نام ویژه‌ی Dockerfile ذخیره می‌کنیم (بدون پسوند) و این Dockerfile را دقیقا در کنار پوشه‌ی mysite قرار می‌دهیم (داخل پوشه‌ی c:\users\vahid) تا کار کپی را از همینجا شروع کند.
سپس برای اجرای این فایل، بجای دستور docker commit آخر، از دستور زیر استفاده می‌کنیم:
 docker build -f Dockerfile -t mysite:nginx-df .
البته می‌توان f Dockerfile- را نیز از این دستور حذف کرد؛ چون مقدار پیش‌فرض آن است (مگر آنکه بخواهیم مسیر خاصی را دقیقا مشخص کنیم):
 docker build -t mysite:nginx-df .
در هر دو دستور آخری که ذکر شدند، در انتهای دستور، یک نقطه نیز قرار دارد که به آن build context گفته می‌شود؛ یا دقیقا همین پوشه‌ای که در آن قرار داریم (c:\users\vahid).
تگ این image را نیز متفاوت با قبلی‌ها انتخاب کرده‌ایم؛ nginx-df بجای مقدار قبلی.
در این حالت اگر دستور آخر را اجرا کنیم، دستور docker images گزارش اضافه شدن این image جدید را ارائه خواهد داد.

مرجع کامل ساخت Dockerfileها را در اینجا می‌توانید مطالعه کنید.


ساخت یک image سفارشی برای هاست یک وب سایت استاتیک در IIS

تا اینجا از وب سرور لینوکسی nginx برای هاست وب سایت استاتیک خود استفاده کردیم. در ادامه می‌خواهیم از وب سرور IIS برای اینکار استفاده نمائیم. بنابراین ابتدا نیاز است یا از ویندوز سرور استفاده کنیم و یا می‌توان با کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز، به Windows Containers سوئیچ کرد و سپس به صورت زیر عمل نمود.
اینبار محتوای Dockerfile ای که کنار پوشه‌ی mysite قرار می‌گیرد، به صورت زیر خواهد بود:
FROM microsoft/iis:nanoserver

COPY mysite c:/inetpub/wwwroot
کار با image اصلی iis با tag مخصوص nanoserver که کم حجم‌تر است، شروع می‌شود. سپس فایل‌های mysite به پوشه‌ی wwwroot این وب سرور کپی خواهد شد.
در ادامه با استفاده از دستور زیر و اجرای فایل Dockerfile، این image جدید را با tag ای به نام iis ایجاد می‌کنیم:
 docker build -t mysite:iis .
پس از آن دستورات docker images و docker ps را جهت مشاهده‌ی وضعیت این image جدید اجرا کنید.


به اشتراک گذاری imageهای سفارشی در Docker Hub

برای به اشتراک گذاری imageهای سفارشی خود در Docker Hub، نیاز است tag آن‌ها را توسط دستور docker tag مطابق فرمت ویژه‌ی docker hub ویرایش کرد:
 docker tag mysite:nginx-df my_user_name/some_name:new_tag_name
در این دستور، Tag فعلی، با ذکر نام کاربری، نام مخزنی جدید در docker hub و سپس یک tag دلخواه، ویرایش می‌شود.
و در آخر برای انتشار آن می‌توان از دستور docker push استفاده کرد:
 docker push my_user_name/some_name:new_tag_name
اگر در اینجا پیام خطای unauthorized را مشاهده کردید، ابتدا دستور docker login را اجرا کنید تا بتوانید به سایت docker hub لاگین کنید (بر اساس مشخصات اکانت خود در داکر هاب) و سپس دستور فوق را اجرا نمائید.
پس از پایان کار اگر به سایت docker hub و مخازن خود مراجعه کنید، این image جدید قابل مشاهده خواهد بود.
نظرات اشتراک‌ها
زندگی پس از Google Reader؛ نگاهی به گزینه‌های مهیا
برنامه gReader تحت اندروید که قبلا فقط با گودر کار می‌کرد در بروز رسانی‌های جدید با Feedly کار می‌کنه و UI برنامه را هم ارتقا داده به نحوی که سرعت کار کردن با اون به وضوح افزایش یافته. امکانات اون هم بیشتر شده نسبت به نسخه‌های قبلی