Delegates – Code Example

נראה קוד שמשתמש ב-delegates ומנצל את היכולות שלו. נבנה הרשמה למועדון לחברות. היתרון של שימוש ב-delegates הוא האפשרות לשימוש חוזר לולידציה של שדות. המטרה היא לנתק את הולידציה ומשדה ספציפי. יהיה אפשר להשתמש בקוד בשימוש חוזר ולייצר קוד גמיש.

יצירת הפרוייקט

ניצור console app (.Net framework) בשם ClubMembershipApplication. בתוך הפרוייקט ניצור תיקיות Models שבה יהיו ה-classes שלנו. בתוכה ניצור class של User. המודל הזה יחזיק את המידע של המשתמש בתהליך ההרשמה.

אנחנו מתחילים מבניית המודל ואז יוצרים את המבנה של ה-DB מתוך הקוד. זאת גישה של Code First. אפשר גם ליצור את ה-DB קודם ואז לגשת אליו מהקוד. כאן אנחנו מייצרים מודל כ-class ואז מריצים פקודות ליצור את המבנה ב-DB.

User.cs

public class User
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string? EmailAddress { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Password { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string PhoneNumber { get; set; }
    public string AddressFirstLine { get; set; }
    public string AddressSecondtLine { get; set; }
    public string AddressCity { get; set; }
    public string PostCode { get; set; }
}

ה-decorator שהוספנו: [DatabaseGenerated(DatabaseGeneratedOption.Identity)] מציין שה-ID בטבלה יהיה שדה ה-Id. השדה יקודם אוטומטית בכל שורה שתתווסף ל-DB. עכשיו צריך להתקין את החבילה של Entity Framework. נתקין את Microsoft.EntityFrameworkCore.Sqlite מתוך חלון Nugets.

יצירת ה-DB

עכשיו ניצור תיקייה בשם Data ונוסיף אליה class בשם ClubMembershipDbContext. ה-class הזה יורש מ-DbContext. נדרוס את פונ' OnConfigurring שמייצרת את הגדרות הקישור ל-DB.

ClubMembershipDbContext.cs

public class ClubMembershipDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite($"Data Source={AppDomain.CurrentDomain.BaseDirectory}ClubMembershipDb.db");
        base.OnConfiguring(optionsBuilder);
    }
}

אחר כך ניצור את הטבלה שרלוונטית ל-class שלנו. כרגע יש לנו את ה-User.

public class ClubMembershipDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite($"Data Source={AppDomain.CurrentDomain.BaseDirectory}ClubMembershipDb.db");
        base.OnConfiguring(optionsBuilder);
    }

    public DbSet<User> Users { get; set; }
}

EF ישתמש ב-Users כדי להוסיף מידע לטבלה. כדי להשתמש בזה נצטרף להוסיף Migration. נוסיף את Entity Framework Core Tools דרך Nugets ואז נפתח את הקונסול של ה-Nugets ונוסיף:

Add-Migration FirstCreate

נוספה לנו תיקיית Migrations ושם קוד ליצירת הטבלה שלנו.

כדי להריץ את ה-Migration נריץ בקונסול Nugets את הפקודה:

update-database

בתיקיית הפרוייקט בתוך bin/debug נוכל לראות את קובץ db שנוצר. כדי לצפות במה שנוצר אפשר להוריד את DB Browser for SQLite.

Class ה-Delegates

תחת אותו solution ניצור פרוייקט חדש מסוג class library. נשתמש בו ליצירת validation רב פעמי. נקרא לו FieldValidationAPI.

נסיר את ה-class שמגיע עם הפרויקט וניצור חדש בשם CommonFieldValidatorFunctions. נהפוך את ה-class ל-public. ניצור 5 הגדרות delegates. לכל אחת נייצר משתנה.

// See that a field is not empty
public delegate bool RequiredValidDel(string fieldVal);
// Constrain a value of a field between min and max value
public delegate bool StringLengthValidDel(string fieldVal, int min, int max);
// Date validator
public delegate bool DateValidDel(string fieldVal, out DateTime validDateTime);
// Valid a field value compare to a regular expression
public delegate bool PattenMatchValidDel(string fieldVal, string pattern);
// Validate a text field compare to another text field
public delegate bool CompareFieldsValidDel(string fieldVal, string fieldValCompare);

public class CommonFieldValidatorFunctions
{
    private static RequiredValidDel _requiredValidDel = null;
    private static StringLengthValidDel _stringLengthValidDel = null;
    private static DateValidDel _dateValidDel = null;
    private static PattenMatchValidDel _pattenMatchValidDel = null;
    private static CompareFieldsValidDel _compareFieldsValidDel = null;
}

ה-delegates הם סוג נתונים המייצג פונקציה עם חתימה מסוימת. הם מאפשרים להעביר פונקציות כפרמטרים ולהפעיל אותן באופן דינמי.

למשל: RequiredValidDel הוא delegate המייצג פונקציה שמקבלת מחרוזת ומחזירה ערך בוליאני המצביע אם השדה אינו ריק. וכן הלאה בכל הגדרות ה-delegates.

השדות הסטטיים משמשים לאחסון delegate instances.

שדה static מקושר ל-class עצמו ולא למופע מסויים של ה-class. לא משנה כמה מופעים של ה-class ניצור, המשתנה הסטטי יהיה קיים רק פעם אחת, כל המופעים של ה-class חולקים את אותו המשתנה. למשתנים הסטטים אפשר לגשת גם בלי ליצור מופע של ה-class.

השדות הסטטיים בקוד מאחסנים את ה-delegates של פונקציות הולידציה השונות, לכל שדה כזה נבנה מאוחר יותר property שתאתחל אותו במידת הצורך. ברגע שניצור את ה-delegate נאחסן אותו לשימוש חוזר.

נכתוב את היישום של ה-delegates שיצרנו. אלו הפונקציות שנרצה שה-delegate יקרא להן כשמפעילים אותו.

public class CommonFieldValidatorFunctions
{
    private static RequiredValidDel _requiredValidDel = null;
    private static StringLengthValidDel _stringLengthValidDel = null;
    private static DateValidDel _dateValidDel = null;
    private static PattenMatchValidDel _pattenMatchValidDel = null;
    private static CompareFieldsValidDel _compareFieldsValidDel = null;

    private static bool RequiredFieldValid(string fieldVal)
    {
        if (!string.IsNullOrEmpty(fieldVal))
            return true;
        return false;
    }

    private static bool StringFieldLengthValid(string fieldVal, int min, int max)
    {
        if (fieldVal.Length >= min && fieldVal.Length <= max)
            return true;
        return false;
    }

    private static bool DateFieldValid(string dateTime, out DateTime validDateTime)
    {
        if (DateTime.TryParse(dateTime, out validDateTime))
            return true;
        return false;
    }

    private static bool FieldPatternValid(string fieldVal, string regularExpressionPattern)
    {
        Regex regex = new Regex(regularExpressionPattern);

        if (regex.IsMatch(fieldVal))
            return true;
        return false;
    }

    private static bool FieldComparisonValid(string field1, string field2)
    {
        if (field1.Equals(field2))
            return true;
        return false;
    }
}

עכשיו נחשוף את הפונקציות שהן private כדי שיהיה אפשר להשתמש בהן. ניצור property מתאים לכל delegate. אנחנו משתמשים פה ב-Singleton Pattern כדי להיות בטוחים שרק מופע אחד של כל אובייקט יווצר.

public class CommonFieldValidatorFunctions
{
    private static RequiredValidDel _requiredValidDel = null;
    ...

    public static RequiredValidDel RequiredFieldValidDel
    {
        get
        {
            if (_requiredValidDel == null)
                _requiredValidDel = new RequiredValidDel(RequiredFieldValid);
            return _requiredValidDel;
        }
    }

    public static StringLengthValidDel StringLengthFieldValid
    {
        get
        {
            if (_stringLengthValidDel == null)
                _stringLengthValidDel = new StringLengthValidDel(StringFieldLengthValid);
            return _stringLengthValidDel;
        }
    }

    public static DateValidDel DateFieldValidDel
    {
        get
        {
            if (_dateValidDel == null)
                _dateValidDel = new DateValidDel(DateFieldValid);
            return _dateValidDel;
        }
    }

    public static PattenMatchValidDel PattenMatchValidDel
    {
        get
        {
            if (_pattenMatchValidDel == null)
                _pattenMatchValidDel = new PattenMatchValidDel(FieldPatternValid);
            return _pattenMatchValidDel;
        }
    }

    public static CompareFieldsValidDel FieldsCompareValidDel
    {
        get
        {
            if (_compareFieldsValidDel == null)
                _compareFieldsValidDel = new CompareFieldsValidDel(FieldComparisonValid);
            return _compareFieldsValidDel;
        }
    }

    private static bool RequiredFieldValid(string fieldVal)
    {
        if (!string.IsNullOrEmpty(fieldVal))
            return true;
        return false;
    }

    ...
}

לסיכום עד כה:

השדות ה-static ב-CommonFieldValidatorFunctions מאחסנים מופעים של delegates שנטענים באופן חד פעמי כאשר יש בהם שימוש. כל delegate נוצר פעם אחת ומוכן לשימוש חוזר כאשר יש אליו גישה מכל מקום ללא צורך ביצירת מופע של ה-class.

ניצור class בשם CommonRegularExpressionValidationPatterns. ה-class יכיל ביטויים שבהם יעשה שימוש בתוכנית שלנו. את הביטויים אפשר ליצר על ידי מחולל regex כלשהו.

CommonRegularExpressionValidationPatterns.cs

public static class CommonRegularExpressionValidationPatterns
{
    public const string Email_Address_RegEx_Pattern = @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$";

    public const string Uk_PhoneNumber_RegEx_Pattern = @"^\(?(?:(?:0(?:0|11)\)?[\s-]?\(?|\+)44\)?[\s-]?\(?(?:0\)?[\s-]?\(?)?|0)(?:\d{2}\)?[\s-]?\d{4}[\s-]?\d{4}|\d{3}\)?[\s-]?\d{3}[\s-]?\d{3,4}|\d{4}\)?[\s-]?(?:\d{5}|\d{3}[\s-]?\d{3})|\d{5}\)?[\s-]?\d{4,5}|8(?:00[\s-]?11[\s-]?11|45[\s-]?46[\s-]?4\d))(?:(?:[\s-]?(?:x|ext\.?\s?|\#)\d+)?)$";

    public const string Uk_Post_Code_RegEx_Pattern = @"([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?))))\s?[0-9][A-Za-z]{2})";

    public const string Strong_Password_RegEx_Pattern = @"(?=^.{6,10}$)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&amp;*()_+}{&quot;:;'?/&gt;.&lt;,])(?!.*\s).*$";
}

הגדרת מבנה לולידציות

עכשיו נחזור לתוכנית הראשית ושם ניצור תיקייה חדשה בשם FieldValidators. כל class יתאים ל-view ספציפי.

בתוך התיקייה ניצור Interface בשם IFieldValidator. בתוכו ניצור delegate שיוכל לבצע ולידציות על כל טופס שיהיה לנו. כרגע נתחיל עם טופס של פרטי משתמש, אבל נניח שאנחנו רוצים לבצע ולידציות על טופס של פרטי בנק, נוכל ליצור class למטרה הזאת ואז נוכל לפנות ל-validator המתאים ולבצע ולידציה לשדות.

השדה: fieldIndex מאפשר להתייחס לשדה מסויים בתוך מערך של שדות.

השדה: fieldValue מייצג את ערך השדה שנשלח לולידציה.

השדה: fieldArray מאחסן את השדות שעברו ולידציה ל-view מסויים.

השדה: fieldInvalidMessage מאפשר להודעת שגיאה להגיע חזרה לקוד שקרא לולידציה.

IFieldValidator.cs

public delegate bool FieldValidatorDel(int fieldIndex, string fieldValue, string[] fieldArray, out string fieldInvalidMessage);

public interface IFieldValidator
{
}

בתוך ה-interface נגדיר:

InitialiseValidatorDelegates – פונ' לאתחול של ה-delegate ל-view מסויים.

FieldArray – מערך שמאחסן את ערכי השדות לבדיקה.

ValidatorDel – אחראי על ה-delegate שצריך להשתמש בו.

public delegate bool FieldValidatorDel(int fieldIndex, string fieldValue, string[] fieldArray, out string fieldInvalidMessage);
public interface IFieldValidator
{
    void InitialiseValidatorDelegates();
    string[] FieldArray { get; }
    FieldValidatorDel ValidatorDel { get; }
}

ניצור מערך שמכיל ערכי שדות. בתיקיית FieldValidators ניצור class בשם FieldConstants.

FieldConstants.cs

public class FieldConstants
{
    public enum UserRegistrationField
    {
        EmailAddress,
        FirstName,
        LastName,
        Password,
        PasswordCompare,
        DateOfBirth,
        PhoneNumber,
        AddressFirstLine,
        AddressSecondLine,
        AddressCity,
        PostCode
    }
}

מימוש IFieldValidator

עכשיו ניצור את ה-class שיממש את ה-interface בשם UserRegistrationValidator.

בראש, הגדרת קבועים לגבי האורך של הקלט האפשרי והגדרת delegate לבדיקה האם כתובת המייל כבר קיימת ב-DB.

UserRegistrationValidator.cs

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    const int FirstName_Max_Length = 100;
    const int LastName_Min_Length = 2;
    const int LastName_Max_Length = 100;

    // Check if the user email already exists in users table
    delegate bool EmailExistsDel(string emailAddress);
}

אחר כך הגדרת משתנים שיחזיקו את ה-delegates לפונקציות הולידציה.

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    ...

    FieldValidatorDel _fieldValidatorDel = null;

    RequiredValidDel _requiredValidDel = null;
    StringLengthValidDel _stringLengthValidDel = null;
    DateValidDel _dateValidDel = null;
    PattenMatchValidDel _pattenMatchValidDel = null;
    CompareFieldsValidDel _compareFieldsValidDel = null;

    EmailExistsDel _emailExistsDel = null;
}

את כל הערכים של הטופס אנחנו רוצים לאחסן במערך. המערך נוצר כאשר ניגשים אליו בפעם הראשונה.

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    ...

    FieldValidatorDel _fieldValidatorDel = null;
    ...

    string[] _fieldArray = null;

    public string[] FieldArray
    {
        get
        {
            if (_fieldArray == null)
                _fieldArray = new string[Enum.GetValues(typeof(FieldConstants.UserRegistrationField)).Length];
            return _fieldArray;
        }
    }
}

ניצור נקודת כניסה ל-delegate של ה-validation ונאתחל את כל המופעים של ה-delegate שנצטרך.

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    ...

    FieldValidatorDel _fieldValidatorDel = null;
    ...

    string[] _fieldArray = null;
    ...

    public FieldValidatorDel ValidatorDel => _fieldValidatorDel;

    public void InitialiseValidatorDelegates()
    {
        _fieldValidatorDel = new FieldValidatorDel(ValidateField);

        _requiredValidDel = CommonFieldValidatorFunctions.RequiredFieldValidDel;
        _stringLengthValidDel = CommonFieldValidatorFunctions.StringLengthFieldValid;
        _dateValidDel = CommonFieldValidatorFunctions.DateFieldValidDel;
        _pattenMatchValidDel = CommonFieldValidatorFunctions.PattenMatchValidDel;
        _compareFieldsValidDel = CommonFieldValidatorFunctions.FieldsCompareValidDel;
    }
}

עכשיו אפשר לעבור על השדות ולבצע להם ולידציה כשבקוד פה רואים את הוולידציה של שדה האימייל.

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    ...

    FieldValidatorDel _fieldValidatorDel = null;
    ...

    string[] _fieldArray = null;
    ...

    public FieldValidatorDel ValidatorDel => _fieldValidatorDel;
    ...

    private bool ValidateField(int fieldIndex, string fieldValue, string[] fieldArray, out string fieldInvalidMessage)
    {
        fieldInvalidMessage = "";

        FieldConstants.UserRegistrationField userRegistrationField = (FieldConstants.UserRegistrationField)fieldIndex;

        switch (userRegistrationField) 
        {
            case FieldConstants.UserRegistrationField.EmailAddress:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Email_Address_RegEx_Pattern)) ? $"You must enter a valid email address{Environment.NewLine}" : fieldInvalidMessage;
                break;
        }

        return (fieldInvalidMessage == "");
    }
}

הקובץ השלם:

public class UserRegistrationValidator : IFieldValidator
{
    const int FirstName_Min_Length = 2;
    const int FirstName_Max_Length = 100;
    const int LastName_Min_Length = 2;
    const int LastName_Max_Length = 100;

    // Check if the user email already exists in users table
    delegate bool EmailExistsDel(string emailAddress);

    FieldValidatorDel _fieldValidatorDel = null;

    RequiredValidDel _requiredValidDel = null;
    StringLengthValidDel _stringLengthValidDel = null;
    DateValidDel _dateValidDel = null;
    PattenMatchValidDel _pattenMatchValidDel = null;
    CompareFieldsValidDel _compareFieldsValidDel = null;

    EmailExistsDel _emailExistsDel = null;

    string[] _fieldArray = null;

    public string[] FieldArray
    {
        get
        {
            if (_fieldArray == null)
                _fieldArray = new string[Enum.GetValues(typeof(FieldConstants.UserRegistrationField)).Length];
            return _fieldArray;
        }
    }

    public FieldValidatorDel ValidatorDel => _fieldValidatorDel;

    public void InitialiseValidatorDelegates()
    {
        _fieldValidatorDel = new FieldValidatorDel(ValidateField);

        _requiredValidDel = CommonFieldValidatorFunctions.RequiredFieldValidDel;
        _stringLengthValidDel = CommonFieldValidatorFunctions.StringLengthFieldValid;
        _dateValidDel = CommonFieldValidatorFunctions.DateFieldValidDel;
        _pattenMatchValidDel = CommonFieldValidatorFunctions.PattenMatchValidDel;
        _compareFieldsValidDel = CommonFieldValidatorFunctions.FieldsCompareValidDel;
    }

    private bool ValidateField(int fieldIndex, string fieldValue, string[] fieldArray, out string fieldInvalidMessage)
    {
        fieldInvalidMessage = "";

        FieldConstants.UserRegistrationField userRegistrationField = (FieldConstants.UserRegistrationField)fieldIndex;

        switch (userRegistrationField) 
        {
            case FieldConstants.UserRegistrationField.EmailAddress:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Email_Address_RegEx_Pattern)) ? $"You must enter a valid email address{Environment.NewLine}" : fieldInvalidMessage;
                fieldInvalidMessage = (fieldInvalidMessage == "" && _emailExistsDel(fieldValue)) ? $"This email address already exists{Environment.NewLine}" : fieldInvalidMessage;
                break;

            case FieldConstants.UserRegistrationField.FirstName:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_stringLengthValidDel(fieldValue, FirstName_Min_Length, FirstName_Max_Length)) ? $"{Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)} must be between {FirstName_Min_Length} and {FirstName_Max_Length}{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.LastName:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_stringLengthValidDel(fieldValue, LastName_Min_Length, LastName_Max_Length)) ? $"{Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)} must be between {LastName_Min_Length} and {LastName_Max_Length}{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.Password:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Strong_Password_RegEx_Pattern)) ? $"Your password must contain at least 1 small-case letter, 1 capital letter, 1 special character and the length should be between 6-10 letters{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.PasswordCompare:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_compareFieldsValidDel(fieldValue, fieldArray[(int)FieldConstants.UserRegistrationField.Password])) ? $"Your entry did not match your password{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.DateOfBirth:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_dateValidDel(fieldValue, out DateTime validDateTime)) ? $"You didn't enter a valid date{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.PhoneNumber:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Uk_PhoneNumber_RegEx_Pattern)) ? $"You didn't enter a valid UK phone number{Environment.NewLine}" : fieldInvalidMessage;
                break;
            case FieldConstants.UserRegistrationField.AddressFirstLine:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                break;
            case FieldConstants.UserRegistrationField.AddressSecondLine:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                break;
            case FieldConstants.UserRegistrationField.AddressCity:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                break;
            case FieldConstants.UserRegistrationField.PostCode:
                fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
                fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Uk_Post_Code_RegEx_Pattern)) ? $"You didn't enter a valid UK post code{Environment.NewLine}" : fieldInvalidMessage;
                break;
            default:
                throw new ArgumentException("This field does not exist");
        }

        // If no other message returned, the field is valid
        return (fieldInvalidMessage == "");
    }
}

שמירת הנתונים

נחזור לתיקיית data וניצור Interface בשם IRegister. נגדיר את פונ' Register שתקבל את הערכים התקינים של הטופס ותשלח אותם לשמירה ב-DB.

IRegister.cs

public interface IRegister
{
    // Valid field data from the user input -> to save to the DB
    bool Register(string[] fields);
    bool EmailExists(string emailAddress);
}

ניצור עוד Interface בשם ILogin שיקבל את פרטי הכניסה ויחזיר את המשתמש המתאים.

ILogin.cs

public interface ILogin
{
    User Login(string emailAddress, string password);
}

ניצור class למימוש IRegister בשם RegisterUser. נוסיף את using ClubMembershipApp.FieldValidators; בראש הקובץ כדי שתהיה לנו גישה ל-constants של האינדקסים של השדות עם השמות שלהם. אחרי יצירת האובייקט נוסיף אותו ל-DB.

RegisterUser.cs

public class RegisterUser : IRegister
{
    public bool EmailExists(string emailAddress)
    {
        throw new NotImplementedException();
    }

    public bool Register(string[] fields)
    {
        using(var dbContext = new ClubMembershipDbContext()) 
        {
            User user = new User
            {
                EmailAddress = fields[(int)FieldConstants.UserRegistrationField.EmailAddress],
                FirstName = fields[(int)FieldConstants.UserRegistrationField.FirstName],
                LastName = fields[(int)FieldConstants.UserRegistrationField.LastName],
                Password = fields[(int)FieldConstants.UserRegistrationField.Password],
                DateOfBirth = DateTime.Parse(fields[(int)FieldConstants.UserRegistrationField.DateOfBirth]),
                PhoneNumber = fields[(int)FieldConstants.UserRegistrationField.PhoneNumber],
                AddressFirstLine = fields[(int)FieldConstants.UserRegistrationField.AddressFirstLine],
                AddressSecondtLine = fields[(int)FieldConstants.UserRegistrationField.AddressSecondtLine],
                AddressCity = fields[(int)FieldConstants.UserRegistrationField.AddressCity],
                PostCode = fields[(int)FieldConstants.UserRegistrationField.PostCode],
            };

            dbContext.Users.Add(user);
            dbContext.SaveChanges();
        }
        return true;
    }
}

נשאר לנו עוד לממש את הפונ' שבודקת האם האימייל קיים.

public bool EmailExists(string emailAddress)
{
    bool emailExists = false;

    using(var dbContext = new ClubMembershipDbContext()) 
    {
        emailExists = dbContext.Users.Any(user => user.EmailAddress.ToLower().Trim() == emailAddress.ToLower().Trim());
    }

    return emailExists;
}

ניצור class למימוש ILogin בשם LoginUser.

public class LoginUser : ILogin
{
    public User Login(string emailAddress, string password)
    {
        User user = null;

        using (var dbContext = new ClubMembershipDbContext()) {
            user = dbContext.Users.FirstOrDefault(u => u.EmailAddress.Trim().ToLower() == emailAddress.Trim().ToLower() && u.Password.Equals(password));
        }
        return user;
    }
}

ועכשיו אפשר לחזור ל-email validator

UserRegistrationValidator.cs

IRegister? _register = null;

public void InitialiseValidatorDelegates()
{
    _fieldValidatorDel = new FieldValidatorDel(ValidateField);
    _emailExistsDel = new EmailExistsDel(_register.EmailExists);

    _requiredValidDel = CommonFieldValidatorFunctions.RequiredFieldValidDel;
    _stringLengthValidDel = CommonFieldValidatorFunctions.StringLengthFieldValid;
    _dateValidDel = CommonFieldValidatorFunctions.DateFieldValidDel;
    _pattenMatchValidDel = CommonFieldValidatorFunctions.PattenMatchValidDel;
    _compareFieldsValidDel = CommonFieldValidatorFunctions.FieldsCompareValidDel;
}

private bool ValidateField(int fieldIndex, string fieldValue, string[] fieldArray, out string fieldInvalidMessage)
{
    fieldInvalidMessage = "";

    FieldConstants.UserRegistrationField userRegistrationField = (FieldConstants.UserRegistrationField)fieldIndex;

    switch (userRegistrationField) 
    {
        case FieldConstants.UserRegistrationField.EmailAddress:
            fieldInvalidMessage = (!_requiredValidDel(fieldValue)) ? $"You must enter a value for field: {Enum.GetName(typeof(FieldConstants.UserRegistrationField), userRegistrationField)}{Environment.NewLine}" : "";
            fieldInvalidMessage = (fieldInvalidMessage == "" && !_pattenMatchValidDel(fieldValue, CommonRegularExpressionValidationPatterns.Email_Address_RegEx_Pattern)) ? $"You must enter a valid email address{Environment.NewLine}" : fieldInvalidMessage;
            fieldInvalidMessage = (fieldInvalidMessage == "" && !_emailExistsDel(fieldValue)) ? $"This email address already exists{Environment.NewLine}" : fieldInvalidMessage;
            break;

...
}

public UserRegistrationValidator(IRegister register)
{
    _register = register;
}

נוסיף 2 classes תומכים.

CommonOutputText.cs

public static class CommonOutputText
{
    private static string MainHeading
    {
        get
        {
            string heading = "Cycling Club";
            return $"{heading}{Environment.NewLine}{new string('-', heading.Length)}";
        }
    }

    private static string RegistrationHeading
    {
        get
        {
            string heading = "Register";
            return $"{heading}{Environment.NewLine}{new string('-', heading.Length)}";
        }
    }

    private static string LoginHeading
    {
        get
        {
            string heading = "Login";
            return $"{heading}{Environment.NewLine}{new string('-', heading.Length)}";
        }
    }

    public static void WriteMainHeading()
    {
        Console.Clear();
        Console.WriteLine(MainHeading);
        Console.WriteLine();
        Console.WriteLine();
    }

    public static void WriteLoginHeading()
    {
        Console.Clear();
        Console.WriteLine(MainHeading);
        Console.WriteLine();
        Console.WriteLine();
    }

    public static void WriteRegistrationHeading()
    {
        Console.Clear();
        Console.WriteLine(MainHeading);
        Console.WriteLine();
        Console.WriteLine();
    }
}

CommonOutputFormat.cs

public enum FontTheme
{
    Default,
    Danger,
    Success
}

public static class CommonOutputFormat
{
    public static void ChangeFontColor(FontTheme fontTheme)
    {
        if(fontTheme == FontTheme.Danger) {
            Console.BackgroundColor = ConsoleColor.Red;
            Console.ForegroundColor = ConsoleColor.White;
        }
        else if (fontTheme == FontTheme.Success) {
            Console.BackgroundColor = ConsoleColor.Green;
            Console.ForegroundColor = ConsoleColor.White;
        } else {
            Console.ResetColor();
        }
    }
}

יצירת התצוגה

ניצור תיקייה חדשה Views.

IView.cs

public interface IView
{
    void RunView();
    IFieldValidator FieldValidator { get; set; }
}

UserRegistrationView.cs

public class UserRegistrationView : IView
{
    IFieldValidator _fieldValidator = null;
    IRegister _register = null;

    public IFieldValidator FieldValidator { get => _fieldValidator; }

    public UserRegistrationView(IRegister register, IFieldValidator fieldValidator)
    {
        _fieldValidator = fieldValidator;
        _register = register;
    }

    public void RunView()
    {
        CommonOutputText.WriteRegistrationHeading();
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.EmailAddress] = GetInputFromUser(FieldConstants.UserRegistrationField.EmailAddress, "Please enter your email address: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.FirstName] = GetInputFromUser(FieldConstants.UserRegistrationField.FirstName, "Please enter your first name: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.LastName] = GetInputFromUser(FieldConstants.UserRegistrationField.LastName, "Please enter your last name: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.Password] = GetInputFromUser(FieldConstants.UserRegistrationField.Password, $"Please enter your password.
            {Environment.NewLine}Your password must contain at least 1 small-case letter,{Environment.NewLine}1 Capital letter, 1 digit, 1 special character{Environment.NewLine} and the length should be between 6-10 characters: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.PasswordCompare] = GetInputFromUser(FieldConstants.UserRegistrationField.PasswordCompare, "Please re-enter your email address: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.DateOfBirth] = GetInputFromUser(FieldConstants.UserRegistrationField.DateOfBirth, "Please enter your date of birth: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.AddressFirstLine] = GetInputFromUser(FieldConstants.UserRegistrationField.AddressFirstLine, "Please enter your first adress line: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.AddressSecondLine] = GetInputFromUser(FieldConstants.UserRegistrationField.AddressSecondLine, "Please enter your second adress line: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.AddressCity] = GetInputFromUser(FieldConstants.UserRegistrationField.AddressCity, "Please enter your address city: ");
        _fieldValidator.FieldArray[(int)FieldConstants.UserRegistrationField.PostCode] = GetInputFromUser(FieldConstants.UserRegistrationField.PostCode, "Please enter your postal code: ");

        RegisterUser();
    }

    private void RegisterUser()
    {
        _register.Register(_fieldValidator.FieldArray);

        CommonOutputFormat.ChangeFontColor(FontTheme.Success);
        Console.WriteLine("You have successfully registered. Please press any key to login.");
        CommonOutputFormat.ChangeFontColor(FontTheme.Default);
    }

    private string GetInputFromUser(FieldConstants.UserRegistrationField field, string promptText)
    {
        string fieldVal = "";

        // The prompt will go on until a valid field value will be entered.
        do {
            Console.Write(promptText);
            fieldVal = Console.ReadLine();
        } while (!FieldValid(field, fieldVal));

        return fieldVal;
    }

    private bool FieldValid(FieldConstants.UserRegistrationField field, string fieldValue)
    {
        if (!_fieldValidator.ValidatorDel((int)field, fieldValue, _fieldValidator.FieldArray, out string invalidMessage)) {
            CommonOutputFormat.ChangeFontColor(FontTheme.Danger);
            Console.WriteLine(invalidMessage);
            CommonOutputFormat.ChangeFontColor(FontTheme.Default);

            return false;
        }
        return true;
    }
}

UserLoginView.cs

public class UserLoginView : IView
{
    ILogin _loginUser = null;
    public IFieldValidator FieldValidator => null;

    public UserLoginView(ILogin login)
    {
        _loginUser = login;
    }

    public void RunView()
    {
        CommonOutputText.WriteMainHeading();
        CommonOutputText.WriteLoginHeading();

        Console.WriteLine("Please enter your email address");
        string emailAddress = Console.ReadLine();

        Console.WriteLine("Please enter your password");
        string password = Console.ReadLine();

        User user = _loginUser.Login(emailAddress, password);
        if(user != null) {
            WelcomeUserView welcomeUserView = new WelcomeUserView(user);
            welcomeUserView.RunView();
        } else {
            Console.Clear();
            CommonOutputFormat.ChangeFontColor(FontTheme.Danger);
            Console.WriteLine("The credentials that you entered do not match any of our records");
            CommonOutputFormat.ChangeFontColor(FontTheme.Default);
            Console.ReadKey();
        }
    }
}

WelcomeUserView.cs

public class WelcomeUserView : IView
{
    User _user = null;

    public WelcomeUserView(User user)
    {
        _user = user;
    }

    public IFieldValidator FieldValidator => null;

    public void RunView()
    {
        Console.Clear();
        CommonOutputText.WriteMainHeading();

        CommonOutputFormat.ChangeFontColor(FontTheme.Success);
        Console.WriteLine($"Hi {_user.FirstName}!!{Environment.NewLine}Welcome to the Cycling Club!!");
        CommonOutputFormat.ChangeFontColor(FontTheme.Default);
        Console.ReadKey();
    }
}

MainView.cs

public class MainView : IView
{
    public IFieldValidator FieldValidator => null;

    IView _registerView = null;
    IView _loginView = null;

    public MainView(IView registerView, IView loginView)
    {
        _registerView = registerView;
        _loginView = loginView;
    }
    public void RunView()
    {
        CommonOutputText.WriteMainHeading();

        Console.WriteLine("Please press 'l' to login or if you are not yet registered please press 'r'");
        ConsoleKey key = Console.ReadKey().Key;

        if (key == ConsoleKey.R) {
            RunUserRegistrationView();
            RunLoginView();
        }
        else if (key == ConsoleKey.L) {
            RunLoginView();
        }
        else {
            Console.Clear();
            Console.WriteLine("Goodbye");
            Console.ReadKey();
        }
    }

    private void RunUserRegistrationView()
    {
        _registerView.RunView();
    }

    private void RunLoginView()
    {
        _loginView.RunView();
    }
}

תחת התיקייה הראשית ניצור קובץ Factory. ב-main view יש לנו אובייקטים שנוצרים מ-ILogin ו-IRegister. מכיוון ששניהם משמשים את IView הפונקציה תחזיר את ה-view המתאים.

Factory.cs

public static class Factory
{
    public static IView GetMainViewObject()
    {
        ILogin login = new LoginUser();
        IRegister register = new RegisterUser();
        IFieldValidator userRegistrationValidator = new UserRegistrationValidator(register);
        userRegistrationValidator.InitialiseValidatorDelegates();

        IView registerView = new UserRegistrationView(register, userRegistrationValidator);
        IView loginView = new UserLoginView(login);
        IView mainView = new MainView(registerView, loginView);

        return mainView;
    }
}

בתוכנית הראשית ניצור mainView ונריץ את פונקציית RunView. נוכל לעשות את זה כי כל יצירת האובייקט מופשטת.

Program.cs

IView mainView = Factory.GetMainViewObject();
mainView.RunView();

Console.ReadKey();