שיעור על Attribute ושימוש ב - Reflection
את הדוגמא ניתן להוריד
מכאן:
יצא לי בזמן האחרון להסביר כמה פעמים את המושג Attribute - ולכן חשבתי לכתוב פוסט שיסביר את המושג למי שנכנס לעולם התכנות.
כשאנחנו כותבים מחלקות בדרך כלל יש לנו מאפיינים - המאפיינים הם בעצם מידע ששייך למופע של המחלקה, לדוגמא:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
הגדרנו מחלקה עם שני מאפיינים, מספר ושם, המשמעות של אותם מאפיינים שכשיהיה לנו מופעים של Person, לכל מופע יש את השם שלו והמזהה שלו.
לפעמים אנחנו רוצים להגדיר התנהגות לאובייקטים שלנו - כלומר אנחנו רוצים להגדיר איך האובייקטים יתנהגו במצבים מסויימים, והמידע הזה לא מידע של המחלקה אלא מידע על המחלקה, וזה מילת המפתח בהבדל בין Properties ל - Attribute, מאפיינים מגדירים מידע של המחלקה לעומת Attribute מגדירים מידע והתנהגות על המחלקה.
דוגמא טובה לכך מהעולם האמיתי, חולצה, אם נסתכל על חולצה נראה שכל חולצה היא מופע של אובייקט חולצה שיש לו מאפיינים כמו צבע, גובה, רוחב, ועוד, בנוסף לזאת לכל חולצה יש פתק לבן בצד שמגדיר בכמה מעלות לכבס ואיך לגהץ, המידע הזה אינו מאפיינים של החולצה אלא הגדרת התנהגות איך להשתמש בחולצה.
דוגמא מעולם התכנות - הגדרה איך ה - debugger יראה לנו את האובייקטים, כברירת מחדל כשאנחנו מסתכלים ב - watch על אובייקט, הוא מראה לנו את ה - ToString שלו, במידה ואנחנו רוצים שהוא יראה משהו אחר, אנחנו צריכים להגדיר לו איך אנחנו רוצים שהאובייקט יתנהג בזמן debug (פוסט מפורט בנושא -
כאן) ואנחנו נגדיר אותו בצורה הזאת
[DebuggerDisplay("Name = {Name}")]
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
למעשה אפשר לחלק את העבודה עם Attribute לשלושה.
1. שימוש בהם.
2. הגדרה שלהם.
3. חקירה.
ואני אסביר
95 אחוזים מהעבודה שלנו עם Attribute זה השימוש בהם - כלומר - אנחנו משתמשים ב - Attribute שונים ומשונים שמישהו כתב עבורנו כדי לקבל פונקציונליות מסויימת, (DebuggerDisplay לדוגמא)
אבל לפעמים אנחנו רוצים להגדיר Attribute משלנו (עוד מעט נראה דוגמא). ואז אנחנו בעצם עושים את שלב 2, כלומר מגדירים Attribute משלנו ואחרים יעשו את שלב 1 (כלומר ישתמשו ב - Attribute שלנו).
אבל ללא שלב 3 (חקירה) אין לזה משמעות , במילה חקירה אני מתכוון שמישהו צריך לבדוק מי משתמש ב - Attribute שהגדרנו ולעשות עם זה משהו, נקח לדוגמא את DebuggerDisplay.
ישב מישהו במייקרוסופט והגדיר Attribute שנקרא DebuggerDisplay (שלב 2)
אנחנו משתמשים ב - Attribute הזה ושמים אותו על אובייקטים שלנו (שלב 1)
במידה שאף אחד לא היה מממש את החקירה (שלב 3) היינו יכולים להשתמש ב - Attribute הזה מהיום עד מחר, וה - Debugger לא היה עושה עם זה כלום, היות שכתבו מנגנון שחוקר את האובייקטים שלנו האם השתמשנו ב - DebuggerDisplay יש משמעות לשימוש ב - Attribute הזה.
דוגמא ל - Attribute ללא שלב 3. הוא Optional ו - DefaultParameterValue, הידעתם שאפשר לכתוב (כבר מ - 2.0) קוד כזה:
public void Func(int id,
[Optional, DefaultParameterValue("shlomo")]
string name)
{
}
מה שאומר שכשמישהו יקרא ל - Func הוא יהיה חייב לשלוח ערך עבור id ויוכל לשלוח ערך עבור name אבל אם הוא לא ישלח, name יקבל ערך ברירת מחדל (shlomo).
נסתכל על ה - Attribute הזה.
שלב 2 (הגדרה) קיים, ישב מישהו במייקרוסופט והגדיר את ה - Attribute
שלב 1 (שימוש) קיים, אנחנו משתמשים בזה.
שלב 3 (חקירה) לא קיים לצערנו - אף אחד לא בודק האם אנחנו משתמשים ב - Attribute הזה, ולכן אנחנו יכולים להשתמש ב - Attribute האלו כאוות נפשנו, אבל בפועל כשנפעיל את Func נצטרך לשלוח ערך גם ל - id וגם ל - name, בגלל שהם שכחו לממש את שלב 3.
(הערה: המתכנתים של VB.NET כן מימשו את שלב 3 - ולכן כשנפעיל את הפונקצייה מ - VB.NET לא נהיה חייבים לשלוח ערך ל - name, הערה שנייה: ב - C# 4.0 תקנו את העוול ומימשו את שלב 3 עבור ה - Attribute האלו, לקריאה נוספת,
כאן)
כעת נראה איך אנחנו ממשים את שלב 2 (הגדרה) ושלב 3. (חקירה)
נניח שאנחנו רוצים לממש מנגנון שידע להגיד לנו איזה מתכנת כתב כל מחלקה ופונקצייה ואיזה מתכנת עדכן.
שלב ראשון נממש את שלב 2, ונגדיר שני Attribute.
נייצר פרויקט בשם AttributeDefinition ובו שני מחלקות
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ItemCreatedAttribute : Attribute
{
public string Name { get; private set; }
public string Date { get; private set; }
public ItemCreatedAttribute(string name, string date)
{
Name = name;
Date = date;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class ItemUpdatedAttribute : Attribute
{
public string Name { get; private set; }
public string Date { get; private set; }
public ItemUpdatedAttribute(string name, string date)
{
Name = name;
Date = date;
}
}
כל Attribute הוא מחלקה שהתנאי היחיד שלה שהוא יורש מ - Attribute
נהוג שכל Attribute שם המחלקה מסתיים עם המילה Attribute.
יש לנו שני מאפיינים (שם ותאריך) שניהם מוגדרות כמחרוזת (אני אסביר יותר מאוחר למה אי אפשר DateTime) ובנאי.
בנוסף יש לנו Attribute על המחלקות שלנו שמגדיר איפה ואיך אפשר להשתמש ב - Attribute שלנו.
ה - Attribute מגדיר על מה מותר לשים את ה - Attribute שלנו (הגדרנו שרק על מחלקות ופונקציות) בנוסף על ItemUpdated הגדרנו AllowMultiple=true כדי שיוכלו לשים יותר ממופע אחד שלו על אותו פונקצייה (מכיון שאפשר לעדכן פונקצייה יותר מפעם אחת).
כדעת נראה את שלב 1 (שימוש) נקמפל את הפרויקט וניתן את ה - dll שלנו לכל מי שרוצה לעקוב מי יוצר ומעדכן את המחלקות והפונקציות - שימוש לדוגמא:
יש לי פרויקט שנקרא SomeProject ובו שני מחלקות:
[ItemCreated("Shlomo", "10/10/2007")]
public class Class1
{
[ItemUpdated("Noam", "07/12/2007")]
[ItemCreated("Shlomo", "10/11/2007")]
public void Func1()
{
}
[ItemUpdated("Dudu", "10/10/2008")]
[ItemUpdated("David", "10/11/2007")]
[ItemCreated("Ifat", "10/10/2007")]
public void Func2()
{
}
}
[ItemUpdated("Simon", "07/09/2005")]
[ItemCreated("Noam", "01/01/2000")]
class Manager
{
[ItemUpdated("Dudu", "02/12/2003")]
[ItemCreated("Shlomo", "10/01/2000")]
public void Work()
{
}
[ItemUpdated("David", "12/07/2004")]
[ItemCreated("Ifat", "14/04/20001")]
public void WorkAgain()
{
}
}
כמו שאפשר לראות מתכנתים כתבו ועדכנו את המחלקות והפונקציות בזמנים שונים.
כעת אנחנו צריכים לממש את שלב 3 (חקירה) במידה ולא נעשה את זה אין טעם להשתמש ב - Attribute שלנו.
כתבתי WinForm שאמור לקבל dll לחקירה ולהציג תוצאות ממנו, הוא נראה כך:
כשנבחר dll מסויים נקבל את כל המחלקות ברשימה, כשנבחר מחלקה במידה ויש עליו את ה - Attribute שלנו נוכל לראות תוצאות - לדוגמא אם נבחר את SomeProject נקבל:
איך זה קורה, למעשה כל החקירה של Attribute היא בעזרת Reflection - ונסתכל על הקוד.
בזמן לחיצה ה - LinkLabel (שלושת הנקודות הכחולות ליד תיבת הטקסט) נבצע את הקוד הבא
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
txtAssemblyName.Text = openFileDialog1.FileName;
LoadClasses();
}
בעזרת OpenFileDialog נבחר קובץ כלשהו, ונפעיל פונקצייה בשם LoadClasses
private Assembly _assembly;
private void LoadClasses()
{
try
{
_assembly = Assembly.LoadFile(txtAssemblyName.Text);
Type[] allClasses = _assembly.GetTypes().Where(item => item.IsClass).ToArray();
cmbClass.Items.Clear();
cmbClass.Items.AddRange(allClasses);
lvClass.Items.Clear();
lvMethod.Items.Clear();
}
catch (BadImageFormatException)
{
MessageBox.Show("The file is not a .NET file");
}
}
יש לנו מופע של ,Assembly המחלקה הזאת מייצגת dll דוטנטי כלשהו,
אנחנו מנסים לטעון את ה - dll, ומבקשים את כל ה - Type שהוא מסוג מחלקה, (אנחנו קוראים ל - GetTypes שיחזיר את כל ה - Types שמוגדרים ב - dll (כולל enum, Interface ועוד))
אנחנו מוסיפים את כל מה שקבלנו ל - Combo. (כלומר את כל השמות של המחלקות שיש לנו במערך)
כעת נרשמנו לאירוע Combo_SelectedChange
private void cmbClass_SelectedIndexChanged(object sender, EventArgs e)
{
Type currentType = _assembly.GetType(cmbClass.Text);
lvClass.Items.Clear();
lvMethod.Items.Clear();
ItemCreatedClass(currentType);
ItemUpdatedClass(currentType);
FillMethodCombo(currentType);
}
נקבל את הטיפוס מתוך ה - Assembly לפי השם שלו (מה שבחרנו ב - Combo)
כעת אנחנו הולכים לחקור את ה - Type האם יש בו שימוש ב - ItemCreated או ב - itemUpdated
private void ItemCreatedClass(Type currentType)
{
Type type = typeof(ItemCreatedAttribute);
object[] itemCreatedAtt = currentType.GetCustomAttributes(type, false);
if (itemCreatedAtt.Length == 1)
{
ItemCreatedAttribute att = (ItemCreatedAttribute)itemCreatedAtt[0];
lvClass.Items.Add(new ListViewItem(new string[] { att.Name, att.Date },
lvClass.Groups[0]));
}
}
נפעיל את מתודת GetCustomAttribute על ה - Type שקבלנו - כפרמטר הוא מקבל את סוג ה - Attribute שאנחנו רוצים לחפש ו - false כדי להגיד שאנחנו רוצים לקבל רק את ה - attribute הזה ולא את היורשים שלו.
אנחנו מקבלים מערך של אובייקטים, היות שאנחנו יודעים שלא יכול להיות יותר ממופע אחד של ItemCreated על מחלקה, אנחנו יכולים להגיד שבמידה וקבלנו מערך בגודל של אחד מן הסתם השתמשו ב - Attribute שלנו.
נוציא את המופע מתוך המערך ונמיר אותו מ - object ל - ItemCreated, ונוסיף אותו ל - ListView
באותה צורה נוציא את ה - ItemUpdate וכמו כן נבדוק על המתודות, אני לא אעתיק לכאן את כל הקוד אבל זה הרעיון המרכזי, ותוכלו להוריד את הקוד
מכאן
הערה:
כפי שאולי שמתם לב שם ה - Attribute הוא ItemCreatedAttribute אבל אפשר להשתמש בו בלי המילה Attribute
[ItemUpdated("Simon", "07/09/2005")]