DCSIMG
June 2007 - Posts - Justin myJustin = new Justin( Expriences.Current );

June 2007 - Posts

Learning Validation Application Block With Strippers (Teaser)

קדם דבר: למה צריך וולידציה? (או: "קופים. פשוט קופים.")

יש לנו בעיה בתחום התוכנה היום - המשתמש.

ה-End User מתעקש באופן קבוע להשתמש באפליקציות המורכבות להחריד שפיתחנו.
כל הקונספט מאחורי UI הוא מאוד בעייתי. User Interface אמרתם? לחשוף ממשק קלט ופלט למשתמש? זה רק מוביל לצרות.

ברוב התרשימים המשתמש יופיע כאיש קטן עם ראש גדול.
בתחילת שנות ה-2000 נהוג היה לייצג את המשתמש בתרשימי UML אי-אלו בדמות הבאה  image_thumb[11], לקראת שנת 2005 שנינו את הפרספקטיבה ל-image_thumb[10]  והיום בעידן האופיס 2007 המשתמש נראה כך image_thumb[9].

תרשו לי להציג דמות אחרת למשתמש:

image

 כן - קוף, שימפנזה, אורנג-אוטנג, למור וקופיפים באשר הם.

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

 

את המשפט הבא למד על בשרו כל מתכנת, ארכיטקט, ראש צוות ומנהל פרוייקט:

המשתמש טיפש או מניאק

נראה גם שההתפצלות הזאת נכונה.

image_thumb[4]

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

 

הבעיה שלנו היא במילים פשוטות - האפליקציה שלנו מקבלת "קלט" מאנשים לא מושלמים. אנשים שעשויים לטעות, אנשים שעשויים להטעות, אנשים שהם לא המתכנתים של המערכת.

הגולש שמכניס שהכתובת דוא"ל שלו היא "אני שולתתתת!!!!111"הוא הבעיה שלנו

האיש עסקים שמנסה לקבוע פגישה לשעה 25:00 בלילה הוא הבעיה שלנו

הבחורה ששכחה לבחור כרטיס אשראי (וויזה\מאסטרקארד\...) באתר קניות ומתקשרת אליי בשלוש בלילה זאת הבעיה שלי שאני הופך לבעיה שלכם 

כדי לפתור את הבעיה הזו כל מתכנת באשר הוא הגיע אינטואטיבית לפתרון הבא:

שלב א':  "נבדוק מה המשתמש נכניס"

שלב ב': נפיל את התוכנה, נזרוק הודעת שגיאה, נגרום למסך להבהב באדום כדי לגרום התקף אפליפסיה, נלכוד את המשתמש לנצח במסך הזה עד שהוא יציית או כל פתרון יצירתי אחר.

במהלך מאמר זה נסקור את Validation Application Block של מיקרוסופט שנועד לתת מענה מקיף והולם לבעיות שתיארנו.

 

חלק ראשון: הפתרון הקיים והמצוי ב-Winforms, Console ו-ASP.Net (או: "איך קוראים לך ובת כמה את?")

אנחנו מנהלים מועדון חשפנות.

image_thumb[7]

 

המשך המאמר יפורסם בעתיד הקרוב בבלוג שלי ב-JustinAngel.Net.

אציין כי מדובר במאמר מקיף ומלא שלי, על כל המשתמע מכך ולאחר קריאת המאמר (על עשרות עמודיו) יכיר הקורא את VAB על בוריו.

Question from Tapuz .Net forum: Generics and Anonymous delegates on List<T> With LINQ!

שאלה:

ב-List<T> Collection יש כל מיני Methods שמקבלות כל מיני פרמטרים ג'נאריים. 

List<T>.ConvertAll מקבלת משהו בשם <Converter<T.

List<T>.Exists, List<T>.RemoveAll, List<T>.TrueForAll ו-List<T>.FindXXX מקבלים משהו בשם <Predicate<T.

List<T>.Sort מקבל <IComprar<T.

List<T>.ForEach מקבל <Action<T. 

מה זה כל ה-Methods האלו? מה המחלקות האלו? צריך לעשות Inheritance מהן? שמעתי שאפשר לעשות משהו בשם Anonymous Methods?  מה הקשר ל-Generics?
קראתי את התיעוד ב-MSDN, אבל הוא מעיק והדוגמאות לא ברורות...

 

תשובה:

זאת שאלה מצויינת שמראה את ההבדל בין 1.1 #C לבין 2.0 #C לבין 3.5 #C.

בואו נתחיל בליצור מודל בעיה - יש לנו אוסף של בקבוקי וויסקי.

 

בואו ניצור מחלקה שתייצג את בקבוק וויסקי.

    public class WhiskeyBottle

    {

        public WhiskeyBottle(Color labelColor, string manufacture, int pricePer750MLBottle)

        {

            this.LabelColor = labelColor;

            this.Manufacture = manufacture;

            this.PricePer750MLBottle = pricePer750MLBottle;

        }

 

        private Color _labelColor = Color.Empty;

        public Color LabelColor

        {

            get { return _labelColor; }

            set { _labelColor = value; }

        }

 

        private string _manufacture;

        public string Manufacture

        {

            get { return _manufacture; }

            set { _manufacture = value; }

        }

 

        private int _pricePer750MLBottle;

        public int PricePer750MLBottle

        {

            get { return _pricePer750MLBottle; }

            set { _pricePer750MLBottle = value; }

        }

 

        public override string ToString()

        {

            return string.Format("{0} {1} costs {2}", Manufacture, LabelColor.ToString(), PricePer750MLBottle);

        }

    }

 סה"כ מחלקה עם שלושה מאפיינים (גם באנגלית: Properties) אחד שהוא string (שם היצרן), עוד אחד שהוא int (מחיר לבקבוק סטנדרטי) ועוד אחד מסוג System.Color שמייצג את צבע התווית.

בואו ניצור אוסף (גם באנגלית: Collection) שמייצג את האוסף שראינו למעלה.

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

 

List<T>.FindAll כדוגמה קלאסית

עכשיו בואו נגיד ואנחנו רוצים להשיג <List<WhiskeyBottle שמכיל את כל בקבוקי הוויסקי של Jhonny Walker.

בדוט נט 1.1 היינו עושים את זה ככה:

            List<WhiskeyBottle> JhonnyWalkerBottls = new List<WhiskeyBottle>();

            foreach (WhiskeyBottle bottleToCheckIfJhonnyWalkerAndAddToResult in bottles)

            {

                if (bottleToCheckIfJhonnyWalkerAndAddToResult.Manufacture == "Jhonny Walker")

                    JhonnyWalkerBottls.Add(bottleToCheckIfJhonnyWalkerAndAddToResult);

            }

והיינו מקבלים אוסף עם כל הבקבוקים של Jhonny Walker.

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

נביט על List<T>.FindAll...

רואים שה-List<T>.FindAll מקבל <Predicate<T.

ה-Predicate הזה הוא Delegate שמצביע על מתודה עם חתימה מסויימת.

 מסתבר שה-Delegate הזה מצביע על מתודה שמקבלת T ומחזירה ערך בוליאני. הערך הבוליאני הזה מייצג "האם האובייקט מסוג T עומד בתנאים של הפרדיקט".

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

          ....

 

        private static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

יש לנו מתודה בשם JohnneyWalkerPredicate שמקבלת WhiskeyBottle ומחזירה true אם היצרן הוא JhonnyWalker או false אם לא.

המתודה הזו עומדת בתנאים של ה-delegate הג'נארי שלנו - מקבלת T כלשהו (כאשר T הוא בקבוק WhiskeyBottle) ומחזירה bool.
ולכן נוכל להכניס אותה לתוך <Predicate<T וספציפית לתוך <Predicate<WhiskeyBottle.

נשלח את ה-Predicate שלנו ל-FindAll.

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

            List<WhiskeyBottle> JhonnyWalkerBottles = bottles.FindAll(pred);

ונראה איך זה נראה בהרצה:

מסתבר שקיבלנו אוסף של כל המחלקות שעונות לתנאי שהגדרנו ב-Predicate הג'נארי שלנו.

חשוב להבין שה-<Predicate<T ששולחים ל-<List<T חייב להיות עם אותו T.

עכשיו ניקח את זה עוד צעד קדימה ונגיד ככה: לא בא לי להתחיל לעבוד עם Delegates ולכתוב את ה-Predicate שלי במתודה נפרדת.
אנחנו רוצים שהפריימוורק תדפוק את הראש בקיר על הדברים האלו ושאנחנו נעשה את המינימום הנדרש.

נפנה למשהו שנקרא Anonymous delegates ו\או Anonymous methods.
הרעיון מאחורי הדברים האנונימיים האלו זה ככה: "אתם, המפתחים, יכולים להכתוב מתודות חדשות בתוך מתודות אחרות ולהמיר אותן ל-delegates, ואני הקומפיילר אשבור את הראש להוציא אותן למתודות נפרדות".

            Predicate<WhiskeyBottle> pred = JhonnyWalkerPredicate;

            List<WhiskeyBottle> JhonnyWalkerBottles = bottles.FindAll(pred);

 

 

        private static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

יהפוך ל-

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                  {

                                                      return (bottle.Manufacture == "Jhonny Walker");

                                                  });

נעבור שורה-שורה.

אמרנו שלתוך List<T>.FindAll צריך לשלוח <Predicate<T.
כלומר לתוך List<WhiskeyBottle>.FindAll נשלח <Predicate<WhiskeyBottle.
אז אנחנו צריכים לשלוח delegate שמחזיר bool ומקבל WhiskeyBottle (לפי ההגדרה של <Predicate<T).

מילת המפתח delegate מאפשרת לנו ליצור במקום מתודה (גם באנגלית: Method) שנכנסת לתוך delegate.

עכשיו השאלה היא מה בדיוק המתודה הבלתי-נראית הזאת עושה...

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                       return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 
לכל WhiskeyBottle שנשלח למתודה הזו נחזיר true אם היצרן הוא Jhonny Walker ו-false אם לא.
נראה שזה באמת רץ, יכול להיות הרי שאני משקר לכם.
בואו נפתח את האסמבלי הדוט-נטי שלנו ב-Reflector.
אפשר לראות שהקומפיילר שלנו ייצר מתודה עם שם מוזר משהו (b__0) והיא למעשה מכילה את הקוד שכתבנו בתוך המתודה Main שלנו.
בנוסף, הוא גם ייצר Delegate שהוא <Predicate<WhiskeyBottle.
 
 
List<T>.Find ו-List<T>.FindLast
 
אז ראינו איך להחזיר אוסף של מחלקות שעונות על תנאי שהגדרנו ב-Predicate.

עכשיו נרצה להחזיר רק מחלקה אחת שעונה על התנאי שהגדרנו ב-Predicate.

המתודה List<T>.Find מחזירה את המחלקה הראשונה שעונה על התנאי שהגדרנו,
והמתודה List<T>.FindLast מחזירה את המחלקה האחרונה (בסידור הקיים של ה-List) שעונה על התנאי.

נראה איך הקוד שלנו יראה:

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                               return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

 

            WhiskeyBottle lastJhonnyWalker = bottles.FindLast(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

נריץ ונראה מה אנחנו מקבלים:

נזכור את הסידור שהוספנו על פיו את המחלקות לאוסף:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

ובאמת שה-Red Label הוא הבקבוק הראשון בסידור הקיים של האוסף שעונה לתנאי שהגדרנו, וה-Blue Label הוא האחרון.

List<T>.FindIndex ו-List<T>.FindLastIndex

בדומה מאוד לזוג המתודות הקודמות, רק שהפעם במקום לקבל את המחלקות עצמן, נקבל את האינדקס שלהן בסידור הנוכחי של האוסף.

            int firstJhonnyWalkerInbdex = bottles.FindIndex(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

 

 

            int lastJhonnyWalkerIndex = bottles.FindLastIndex(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

ובהרצה:

 

 

ואיך נידע שאלו באמת האינדקסים של המחלקות באוסף?
נפתח את חלון Quick Watch (או דרך התפריט Debug --> Windows --> Quick Watch, או דרך קיצור המקלדת Ctrl + Alt + Q).

נזין את הביטוי [bottles[firstJhonnyWalkerIndex ונראה שבאמת הוא מצביע על ה-Red Label:

List<T>.Exists

מקבל <Predicate<T ומחזיר אם התנאי נכון לפחות לאחד מהמחלקות באוסף.

            bool weHaveJhonnyWalker = bottles.Exists(delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            });

 

            bool weHaveGleenFiddich = bottles.Exists(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Glenn Fiddich");

                                });

ובהרצה:

וראינו שהיות ויש לנו באוסף בקבוקים שהיצרן שלהן Jhonny Walker קיבלנו true על ה-exists שלו, וקיבלנו false על ה-exists של Glenn Fiddich.

List<T>.TrueForAll

המתודה תחזיר true אם ה-Predicate שישלח אליה יחזיר true לכל המחלקות בה ו-false אם לא.

            bool allBottlesAreJhonnyWalker = bottles.TrueForAll(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Jhonny Walker");

                                });

 

            bool allBottlesHaveManufacture =  bottles.TrueForAll(delegate(WhiskeyBottle bottle)

                                            {

                                                return (!string.IsNullOrEmpty(bottle.Manufacture));

                                            });

ובהרצה:

ובאמת נראה שלא כל הבקבוקים שלנו היצרן שלהם הוא Jhonny Walker, אבל לכל הבקבוקים שלנו אכן יש לפחות שם יצרן.

List<T>.RemoveAll

אנחנו כבר מתורגלים - מקבל Predicate, ומסיר מהאוסף כל מחלקה שעומדת בתנאי. (ועל הדרך מחזיר מספר מחלקות שהוצאו מהאוסף)

            int bottlesRemoved = bottles.RemoveAll(delegate(WhiskeyBottle bottle)

                                {

                                    return (bottle.Manufacture == "Jhonny Walker");

                                });

ובהרצה:

ואחרי ההרצה נראה שבאוסף שלנו נשארו רק הבקבוקי וויסקי שלא יוצרו ע"י Jhonny Walker.

List<T>.ForEach

OK, סיימנו עם המתודות של ה-<Predicate<T.

עכשיו נביט על List<T>.ForEach

אפשר לראות ש-ForEach מקבל <Action<T. נביט על ההגדרה של <Action<T.

מדובר על Delegate ג'נארי של T שמקבל מחלקה מסוג T.

בואו נחליט שאנחנו רוצים לייקר את המחיר של כל הבקבוקים ב-10 ש"ח.

            bottles.ForEach(delegate(WhiskeyBottle bottle)

                                {

                                    bottle.PricePer750MLBottle += 10;

                                });

ולאחר הרצה נוכל לראות שהמחיר אכן התייקר ב-10 ש"ח:

כאשר המחירים המקוריים הם:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

בצורה זו, נוכל לבצע קוד על כל אחד מהמחלקות באוסף שלנו, בצורה שקולה לחלוטין ללולאת ForEach.

מעניין לשים לב להבדל בין ה-<Predicate<T ל-<Action<T המקומפלים.

 

אפשר לראות שכל ה-<Predicate<T המקומפלים שלנו מחזירים boolean וה-<Action<T הוא עם ערך החזרה void.

שימו לב שבשום מקום בקוד לא ציינו במפורשות ב-Anonymous Delegates שאנחנו מחזירים bool או void, הקומפיילר הסיק את זה לבד במהלך קומפילציה.

List<T>.ConvertAll

המתודה List<T>.ConvertAll מחזירה אוסף שהוא העתק של האוסף המקורי אחרי המרה מסוג לסוג. הגדרה מפוצצת, אז נתחיל מדוגמה קטנה.

קודם נביט על ההגדרה של List<T>.ConvertAll.

אנחנו רואים ש-ConvertAll מקבל משהו שנקרא <Converter<T, TOutput. בואו נביט על ההגדרה של ה-<Converter<T, TOutput:

אנחנו רואים שמדובר על delegate עם שני פרמטרים ג'נאריים!
לא רק T כמו מקודם, אלא גם TOutput!

אפשר לראות ה-delegate הזה יקבל מופע של מחלקה מסוג TInput, ויחזיר מופע של מחלקה מסוג TOutput.
כלומר, ה-Converter המוזר הזה יקבל מופע של מחלקה מסוג מסויים וימיר אותה למופע של מחלקה מסוג אחר.

בואו נראה דוגמה יותר פשוטה מהבקבוקי וויסקי שלנו, נראה דוגמה של המרת אוסף stringים לאוסף intים.

            List<string> strings = new List<string>();

            strings.Add("1");

            strings.Add("2");

            strings.Add("3");

נרצה עכשיו לקבל <List<int.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

                                                            return Convert.ToInt32(input);

                                                        });

נעבור על זה שורה שורה.

 List<int> ints =

נרצה להשתמש ב-ConvertAll כדי לקבל את ה-<List<int הזה.

List<int> ints = strings.ConvertAll

מה מסתבר? צריך להגיד ל-ConvertAll בצורה של פרמטר ג'נארי לאיזה סוג אנחנו רוצים להמיר כל מופע של המחלקה.
במקרה שלנו אנחנו ממירים string ל-int, ולכן נגיד ל-ConvertAll שאנחנו ממירים ל-int.

List<int> ints = strings.ConvertAll<int>

עכשיו נרצה להעביר לו את ה-anonymous delegate שאחרי על ההמרה.
ראינו בהגדרה שהוא מקבל מאפיין מסוג TInput, שבמקרה שלנו הוא ה-string שנרצה להמיר.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

 

                                                        });

עכשיו נצטרך להחזיר את ה-input שלנו (שהוא string) אחרי המרה.

            List<int> ints = strings.ConvertAll<int>(delegate(string input)

                                                        {

                                                            return Convert.ToInt32(input);

                                                        });

לפני ההמרה:

אחרי ההמרה:

נסכם:

המרנו אוסף מסוג <List<TInput לאוסף מסוג של <List<TOutput דרך המרה אחד-אחד של TInput ל-TOutput.

בואו נראה את הקוד שיצרנו ב-Reflector:

גם פה, הקומפיילר הסיק לבד שאנחנו ממירים מ-string ל-int.
לא ציינו בשום מקום בתוך המתודה שאנחנו מחזירים int, אבל הקומפיילר ידע להסיק את זה. (גם מתוך הפרמטר הג'נארי של ConvertAll וגם מתוך ה-anonymous delegate).

עכשיו נרצה לעשות משהו עם הבקבוקי וויסקי שלנו. למשל להמיר בקבוקי וויסקי לפורמט XML.

            List<string> bottlesXml = bottles.ConvertAll<string>(delegate(WhiskeyBottle bottle)

                                                                    {

 

                                                                    });

עד כאן ברור יחסית - נרצה להחזיר מערך של <List<string שייצג את ה-XMLים של כל בקבוק ובקבוק.
אמרנו ל-ConvertAll "תמיר בבקשה ל-string" וכתבנו anonymous method שמקבלת בקבוק ותחזיר string.

נרצה להשתמש ב-XmlSerializer בשביל ההמרה של WhiskeyBottle ל-XML.
דבר ראשון בשביל XmlSerializer נוסיף קונסטרקטור ריק למחלקה שלנו.

    public class WhiskeyBottle

    {

        public WhiskeyBottle()

        {

        }

 

       ...

    }


לאתחל XmlSerializer לוקח זמן (במיוחד אם משתמשים בקונסטרקטורים מסויימים שלו), אז נרצה לאתחל אותו רק פעם אחת.

            XmlSerializer bottleSerializer = new XmlSerializer(typeof(WhiskeyBottle));

            List<string> bottlesXml = bottles.ConvertAll<string>(delegate(WhiskeyBottle bottle)

                                                                    {

 

                                                                    });

מסתבר שבתוך ה-anonymous delegate שלנו יש לנו גישה לפרמטרים שנמצאים מחוץ למתודה.

יש לנו גישה לעבוד עם מחלקות שמוצהרות מחוץ ל-anonymous delegate!
מה שבחיים לא היינו יכולים לעשות עם היינו עובדים עם סתם delegate, כי היינו צריכים לשמור על החתימה שלו שלא יקבל עוד פרמטרים!

וככה יראה הקוד שלנו כעת:

            XmlSerializer bottleSerializer = new XmlSerializer(typeof(WhiskeyBottle));

            List<string> bottlesXml = bottles.ConvertAll<string>(

                                        delegate(WhiskeyBottle bottle)

                                        {

                                            return ConvertToXml(bottle, bottleSerializer);

                                        });

למעוניינים, וזה מחוץ להיקף המאמר, הנה הקוד של ConvertToXml: (קוד שכתבתי בשנייה למטרות הדגמה, לא כותבים ככה קוד)

        private static string ConvertToXml(object bottle, XmlSerializer serializer)

        {

            using (MemoryStream memoryStream = new MemoryStream())

            {

                XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);

 

                serializer.Serialize(memoryStream, bottle);

 

                return UTF8ByteArrayToString(memoryStream.ToArray());

            }

        }

 

        private static string UTF8ByteArrayToString(Byte[] characters)

        {

            UTF8Encoding encoding = new UTF8Encoding();

            String constructedString = encoding.GetString(characters);

            return (constructedString);

        }

בואו נראה אם באמת קיבלנו מערך של XMLים שמייצגים את הבקבוקי וויסקי שלנו.

באמת קיבלנו חמש מחרוזות שמייצגות XML. אבל איך נידע שה-XML באמת קשור ל-WhiskeyBottle שלנו?
שמתם לב לזכוכית מגדלת הקטנה שם? נעמוד עליה עם העכבר.

נבחר ב-Xml Visualizer ונראה:

ומלבד ה-LabelColor (היות והוא struct) באמת קיבלנו XML שמייצג את הבקבוקי וויסקי שלנו.

נביט ב-Reflector על הקוד של ה-Anonymous delegate.

אפשר לראות שה-anonymous delegate שלנו לא ייצר הפעם מתודה בצד, אלא ייצר מחלקה שלמה בצד!

למה מחלקה שלמה? כי אנחנו משתמשים ב-XmlSerializer שלנו ואנחנו עדיין צריכה מתודה שניתן להמיר שתתאים ל-<Converter<TInput, TOutput. אז זה הפתרון של הקומפיילר למצב הזה.

וכל זה - קורה בלי התערבות שלנו.

List<T>.Sort

בואו נזכר בכיצד איתחלנו את האוסף שלנו:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

כלומר, רד-לייבל הוא הראשון באוסף, בלאק-לייבל הוא שני ובסוף יושב לו ה-Jack Daniels.

בואו נסדר את האוסף הזה לפי מחיר. אבל נבלגן אותה קצת יותר למטרות הדוגמה:

            List<WhiskeyBottle> bottles = new List<WhiskeyBottle>();

            bottles.Add(new WhiskeyBottle(Color.Blue, "Jhonny Walker", 1780));

            bottles.Add(new WhiskeyBottle(Color.Red, "Jhonny Walker", 120));

            bottles.Add(new WhiskeyBottle(Color.Gold, "Jhonny Walker", 700));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jack Daniels", 280));

            bottles.Add(new WhiskeyBottle(Color.Black, "Jhonny Walker", 250));

נביט על המתודה List<T>.Sort:

Sort מקבל או <IComprarer<T או<Comparison<T.

נעבור על שתי האפשרויות, ונתחיל מ-<Comparison<T. נביט על ההגדרה שלו:

<Comperison<T מקבל שתי מופעים מסוג T ומחזיר איזה int מסתורי.
כנראה שה-int הזה מסמן את התוצאות של ההשוואה בין שני הפרמטרים.

לפי MSDN:
(http://msdn2.microsoft.com/en-us/library/tfakywbh.aspx)

כלומר, אם הפרמטר הראשון "קטן" מהפרמטר השני נחזיר מספר שלילי.
אם הפרמטר הראשון "גדול" מהפרמטר השני נחזיר מספר חיובי.
ואם הם "שווים" נחזיר 0.

נתחיל לכתוב את הקריאה ל-List<T>.Sort:

            bottles.Sort(delegate(WhiskeyBottle x, WhiskeyBottle y)

                            {

                                // return SomeInt;

                            });

עד כאן אנחנו כבר מתורגלים, אז נממש את ה-Sort הזה לפי הכללים שכתובים למעלה באותיות קידוש לבנה.

            bottles.Sort(delegate(WhiskeyBottle x, WhiskeyBottle y)

                            {

                                if (x.PricePer750MLBottle > y.PricePer750MLBottle)

                                    return 1;

                                else if (y.PricePer750MLBottle > x.PricePer750MLBottle)

                                    return -1;

                                else

                                    return 0;

                            });

ובאמת כמו שאמרנו

אם הבקבוק הראשון יותר יקר מהבקבוק השני נחזיר 1

אם הבקבוק השני יותר יקר מהבקבוק הראשון נחזיר1-

אם הם שווים במחירם נחזיר 0

נריץ את זה. לפני הסידור:

אחרי הסידור:

ובאמת סידרנו את האוסף לפי התנאי סידור שהגדרנו.

תחשבו על זה שהגדרנו תנאי נומרי (שמבוסס על השוואה נומרית בין שתי פרמטרים נומריים) אבל ההשוואה יכולה להיות כל השוואה למעשה בין דברים שבכלל לא היינו יכולים להשוות. למשל השוואה בין "אבטיח לאידואלוגיה" נוכל להגיד ש"אבטיח גדול מאידואולוגיה".

אמרנו גם שיש את ה-overload הנוסף של List<T>.Sort שמקבל <IComparer<T.

נביט על הממשק הזה:

הממשק הזה מחייב אותנו ליצור מחלקה עם המתודה Compare שמקבלת שני מופעים מאותו סוג ומחזירה int.

במקום לממש ישירות את <IComprarer<T ניצור מחלקה שיורשת מ-<Comparer<T שהיא מחלקה שיורשת בעצמה את <IComprarer<T. (ירושה מהמחלקה הזו נותנת לנו טיפול במקרי ברירת מחדל וב-nullים).

נירש את המחלקה הזו.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

        }

אנחנו חייבים לממש את המתודה Compare שדיברנו עליה.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

            public override int Compare(WhiskeyBottle x, WhiskeyBottle y)

            {

            }

        }

ובדיוק אותו קוד כמו מקודם.

        public class WhiskeyBottlesPriceSorter : Comparer<WhiskeyBottle>

        {

            public override int Compare(WhiskeyBottle x, WhiskeyBottle y)

            {

                if (x.PricePer750MLBottle > y.PricePer750MLBottle)

                    return 1;

                else if (y.PricePer750MLBottle > x.PricePer750MLBottle)

                    return -1;

                else

                    return 0;

            }

        }

נשלח את המחלקה הזו כפרמטר ל-List<T>.Sort.

            bottles.Sort(new WhiskeyBottlesPriceSorter());

ובאמת נראה שהאוסף שלנו ממויין לפי התנאים שהגדרנו:

ההבדל המהותי הזה בין  anonymous delegate לבין מחלקה קונקרטית עם מתודה מפורשת מעלה שאלה מאוד חשובה מבחינת הנדסת תוכנה.

מתי נשתמש ב-Annoynomus delegates ומתי נשתמש במתודות מפורשות?

חשוב לשים לב שבמאמר זה לא דנו בכלל מתי עובדים עם Anonymous delegates מול מתי עובדים עם מתודות מפורשות.

כלל האצבע המינימלי הוא - למנוע שכפול קוד.

כלומר, אם נכתוב פעמיים אם בשתי מקומות שונים את אותה Anonymous Method, אז יש לנו שכפול קוד ואנחנו רוצים להימנע מזה.
למשל ראינו לא מעט:

                          delegate(WhiskeyBottle bottle)

                                            {

                                                return (bottle.Manufacture == "Jhonny Walker");

                                            }

למשל בשני המתודות הבאות:

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                                            return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                                            return (bottle.Manufacture == "Jhonny Walker");

                                                        });

לא נרצה שיהיה לנו סתם שכפול קוד. אז נעתיק את הקוד לתוך מתודה שיושבת במקום שמתאים לארכיטקטורה שלנו (מחלקה מיוחדת שמחזיקה את כל ה-Predicateים, מחלקות ייעודיות, ועוד אין-ספור אפשרויות). כרגע ניצור את המתודה בתוך מחלקת ה-WhiskeyBottle:

    public class WhiskeyBottle

    {

        public static bool JhonnyWalkerPredicate(WhiskeyBottle bottle)

        {

            return (bottle.Manufacture == "Jhonny Walker");

        }

 

        ....

    }

והקוד שלנו יראה ככה כעת:

            WhiskeyBottle JhonnyWalkerBottles = bottles.FindAll(WhiskeyBottle.JhonnyWalkerPredicate);

            WhiskeyBottle firstJhonnyWalker = bottles.Find(WhiskeyBottle.JhonnyWalkerPredicate);

זה היה הכלל המינימלי - למנוע שכפול קוד.

הכלל המקסימלי הוא - למנוע ערבוב אחריות וסמכויות בין מחלקות.

 למשל, השאלה "האם המתודה שבוחרת את כל בקבוקי ה-Jhonny Walker צריכה לדעת כיצד הבחירה מתבצעת? האם היא צריכה להכיר את המחרוזת "Jhonny Walker" ואת המאפיין Manufacture?".

אם עניתם שלא, המתודה שבוחרת לא צריכה לדעת כיצד הבחירה מתבצעת בפועל (רק להבין את המשמעות שלה), אז לעולם לא נשתמש ב-Anonymous delegates.

אם עניתם שכן, אז נשמור רק על הכלל המינימלי של להימנע משכפול קוד.

איפה LINQ נכנס לעניין?

בדוט נט 3.5 קיבלנו את שפת ה-LINQ  שאחד מהיישומים שלה היא LINQ for objects.

במקום הקוד הזה בדוט נט 1.1:

            List<WhiskeyBottle> JhonnyWalkerBottls = new List<WhiskeyBottle>();

            foreach (WhiskeyBottle bottleToCheckIfJhonnyWalkerAndAddToResult in bottles)

            {

                if (bottleToCheckIfJhonnyWalkerAndAddToResult.Manufacture == "Jhonny Walker")

                    JhonnyWalkerBottls.Add(bottleToCheckIfJhonnyWalkerAndAddToResult);

            }

 ובמקום הקוד הזה שכרגע למדנו בדוט נט 2.0: 

            List<WhiskeyBottle> JhonnyWalkerBottles =

                                    bottles.FindAll(delegate(WhiskeyBottle bottle)

                                                        {

                                           return (bottle.Manufacture == "Jhonny Walker");

                                                        });

נוכל לרשום בדוט נט 3.5:

            IEnumerable<WhiskeyBottle> JhonnyWalkerBottles =

                    from bottle in bottles

                    where bottle.Manufacture == "Jhonny Walker"

                    select bottle;

LINQ for objects בעניין הזה מחליף לנו את צורת הכתיבה בכל הקשור לפרדיקטים.
נוכל לכתוב סינטקס דמוי SQL עם Intellisense מלא שיאפשר לנו לבצע שאילתות על אוספים של מחלקות מידע בזכרון.

למשל במקום הפרדיקט של Find (שהוא למעשה FindFirst) נוכל לכתוב שאילתא שקולה ב-LINQ.

            WhiskeyBottle firstJhonnyWalker = bottles.Find(delegate(WhiskeyBottle bottle)

                                                        {

                                               return (bottle.Manufacture == "Jhonny Walker");

                                                        });

 

כמו שב-SQL יש לנו Top 1 כדי להגיד שאנחנו רוצים רק את הרשומה האחרונה, נוכל להגיד ב-LINQ

            var JhonnyWalkerBottles =

                    (from bottle in bottles

                    where bottle.Manufacture == "Jhonny Walker"

                    select bottle).Take(1);

הקוד שעבדנו עליו זמין להורדה כאן - http://www.JustinAngel.Net/files/Example-ExploringGeneircList.zip.

Question from Tapuz .Net forum: Refactoring code for code review

שאלה:

יש לי מערך של קבציםומערך של אובייקטים המכיל ערך ישן וערך חדש.
אני צריכה להחליף בכל הקבצים את הערך הישן לערך החדש.
אני עשיתי את זה כך / אשמח לשמוע ביקורת לכאן ולכאן או אפשרות לשיפור הביצועים.

מה דעתכם? האם יש דרך לשפר?

        public static void Main()

        {

            //רשימה של השמות הישנים והשמות החדשים על מנת להחליף את הטקסט

            List<manageFiles> mf = new List<manageFiles>();

            mf.Add(new manageFiles("replace me", "with me"));

 

            //רשימת הקבצים החדשה

            List<string> filesUrl = new List<string>();

            filesUrl.Add("myFile1.txt");

            filesUrl.Add("myFile2.txt");

            filesUrl.Add("myFile3.txt");

 

            //מילוי הנתונים

            foreach (string strFile in filesUrl)

            {

                string tochen = "";

                foreach (manageFiles manf in mf)

                {

                    string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                    Regex rg = new Regex(manf.OldName);

                    using (StreamReader sr = new StreamReader(path))

                    {

                        tochen = sr.ReadToEnd();

                    }

                    //החלפת הערכים

                    tochen = rg.Replace(tochen, manf.NewName);

                    //מחיקת הקובץ הישן

                    if (File.Exists(path))

                    {

                        File.Delete(path);

                    }

                    using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                    {

                        sw.Write(tochen);

                    }

                }

            }

        }

ובצד יש את המחלקה:

        internal class manageFiles

        {

            public manageFiles(string newName, string oldName)

            {

                this.newName = newName;

                this.oldName = oldName;

            }

 

            private string newName;

            private string oldName;

 

            public string NewName

            {

                get

                {

                    return newName;

                }

            }

 

            public string OldName

            {

                get

                {

                    return oldName;

                }

            }

        }

תשובה:

 יש מקום לשיפור ונעבור על זה ביחד דרך כללי Refactoring.

קודם נגדיר מילונית (או אבטיחית) מה זה החיה המוזרה הזאת  Refactoring.

Refactoring זה שינוי הקוד הקיים, כך שהוא מבצע בדיוק מה שהוא ביצע קודם, רק כתוב בצורה שונה.

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

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

 

בואו נתחיל.

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

Main זה לא שם של פונקציה. DoIt זה לא שם של פונקציה. Start זה לא שם של פונקציה.

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

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

נסמן את כל הקוד בתוך Main, נלחץ כפתור ימני, נכנס ל-Refactor ונבחר Extract Method.

ואמרנו, אנחנו רוצים שהשם של המתודה (גם באנגלית: Method) יתאר את הקונספט מאחורי מה שהיא מבצעת. ולכן שם הפונקציה החדש יהיה:

ועכשיו הקוד שלנו נראה ככה:

מה הרווחנו מזה? תוכניתן שעכשיו רוצה להבין מה קטע הקוד המפחיד הזה שראינו בשאלה, לפחות יש לו כיוון כלשהו על מה הקוד הזה באמת מבצע.

 

 

כלל שני שנראה היום ב-Refactoring - אל תכתבו הערות.

הערות בקוד זה אינהרנטית רע ומסמנות תחלואה עמוקה יותר בקוד.

אני אשאל אתכם את השאלה הקבועה:

מתי פעם אחרונה קראתם הערה שכתבתם? אולי פעם בחודש-חודשיים עשיתם טובה וקראתם הערה.

מתי פעם אחרונה שיניתם הערה שכתבתם כי היא התיישנה? אף פעם.

לא רק שהרבו המוחץ של תוכניתנים לא קוראים הערות, עוד לא נולד התוכניתן שישנה הערה שכבר כתובה.
אז בסופו של דבר נקבל הערה מיושנת, לא נכונה ומטעה.

במקום לכתוב בהערות מה שצריך להיות כתוב בקוד, נכתוב מה שהתכוונו בקוד עצמו.

 

מסתבר שיש מחלקה בשם managedFiles והמטרה שלה היא להחזיק טקסט שעומדים להחליף והטקסט שיחליף אותו.

למה פשוט לא לקרוא למחלקה ככה?

נילך לשם המחלקה שלנו, נמחק אותו ונכתוב מחדש שם שבאמת מתאר מה המחלקה שלנו מחזיקה.

איזה שם ארוך PairOfTextToReplaceWithTextToReplaceIt.
עכשיו שיבוא תוכניתן וינסה להבין מה המחלקה הזו עושה ומה התפקיד שלה - הוא גם יבין.

שמתם לב שהופיע לנו קו אדום קטן מתחת לשם המחלקה, כאשר נעמוד עליו עם העכבר נקבל אפשרות ש-Visual Studio 2005 ישנה לנו את שם המחלקה בכל מקום שמשתמשים בו.

ובמקום הקוד הזה:

            //רשימה של השמות הישנים והשמות החדשים על מנת להחליף את הטקסט

            List<manageFiles> mf = new List<manageFiles>();

            mf.Add(new manageFiles("replace me", "with me"));

נקבל אוטומטית את הקוד הבא:

            //רשימה של השמות הישנים והשמות החדשים על מנת להחליף את הטקסט

            List<PairOfTextToReplaceWithTextToReplaceIt> mf = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            mf.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

נתנו שם משמעותי למחלקה שאנחנו עובדים איתה.

עכשיו ניתן שם משמעותי למשתנה שאנחנו עובדים איתו. מה זה אוסף בשם mf? אני לא מבין מה האוסף הזה מכיל. השם לא מספיק תיאורי.

אז בואו נחשוב ביחד, זה אוסף של טקסטים להחלפה והטקסטים שמחליפים אותם...נקרא למשתנה TextsToReplaceWithTextToReplaceThem.

נמחק את השם mf ונכתוב במקומו TextsToReplaceWithTextToReplaceThem.

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

נשנה את השם באופן גורף באמצעות כלי ה-Refactoring של VS2005 והקוד הזה:

            //רשימה של השמות הישנים והשמות החדשים על מנת להחליף את הטקסט

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            mf.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            //רשימת הקבצים החדשה

            List<string> filesUrl = new List<string>();

            filesUrl.Add("myFile1.txt");

            filesUrl.Add("myFile2.txt");

            filesUrl.Add("myFile3.txt");

 

            //מילוי הנתונים

            foreach (string strFile in filesUrl)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in mf)

יהפוך לקוד הבא אחריו:

            //רשימה של השמות הישנים והשמות החדשים על מנת להחליף את הטקסט

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            //רשימת הקבצים החדשה

            List<string> filesUrl = new List<string>();

            filesUrl.Add("myFile1.txt");

            filesUrl.Add("myFile2.txt");

            filesUrl.Add("myFile3.txt");

 

            //מילוי הנתונים

            foreach (string strFile in filesUrl)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

אפשר גם כבר למחוק את ההערה הזו למעלה שם היות והיא כבר כתובה בקוד שלנו. והקוד שלנו נראה עכשיו ככה:

  private static void ReplaceFilesContent()

        {

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            //רשימת הקבצים החדשה

            List<string> filesUrl = new List<string>();

            filesUrl.Add("myFile1.txt");

            filesUrl.Add("myFile2.txt");

            filesUrl.Add("myFile3.txt");

 

            //מילוי הנתונים

            foreach (string strFile in filesUrl)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

                {

                    string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                    Regex rg = new Regex(manf.OldName);

                    using (StreamReader sr = new StreamReader(path))

                    {

                        tochen = sr.ReadToEnd();

                    }

                    //החלפת הערכים

                    tochen = rg.Replace(tochen, manf.NewName);

                    //מחיקת הקובץ הישן

                    if (File.Exists(path))

                    {

                        File.Delete(path);

                    }

                    using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                    {

                        sw.Write(tochen);

                    }

                }

            }

        }

נטפל גם בהערה הנוספת "רשימת הקבצים החדשה". שלמעשה זהו אוסף (גם באנגלית: Collection) של שמות קבצים שבהם נעבור ונחליף טקסטים. אז נקרא לזה בדיוק  ככה.

            //רשימת הקבצים החדשה

            List<string> filesUrl = new List<string>();

            filesUrl.Add("myFile1.txt");

            filesUrl.Add("myFile2.txt");

            filesUrl.Add("myFile3.txt");

הופך ל:

            List<string> URLsForFilesToReplaceTheirContent = new List<string>();

            URLsForFilesToReplaceTheirContent.Add("myFile1.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile2.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile3.txt");

 

 

נפנה את תשומת ליבנו לקטע הקוד הבא:

            //מילוי הנתונים

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

                {

                    string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                    Regex rg = new Regex(manf.OldName);

                    using (StreamReader sr = new StreamReader(path))

                    {

                        tochen = sr.ReadToEnd();

                    }

                    //החלפת הערכים

                    tochen = rg.Replace(tochen, manf.NewName);

                    //מחיקת הקובץ הישן

                    if (File.Exists(path))

                    {

                        File.Delete(path);

                    }

                    using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                    {

                        sw.Write(tochen);

                    }

                }

            }

אמרנו כבר, יש כאן הערה, ולכן צריך להוציא את הקוד הזה למתודה נפרדת שהשם שלה יהיה הפונקציונליות שלה.
נסמן את הקוד, נלחץ Ctrl + R, Ctrl + M (קיצור מקלדת ל-Refactor --> Extract Method של העכבר) ונוציא את הקוד הזה למתודה נפרדת.

מה הקוד הזה עושה? לפי מה שנראה הוא עובר על רשימת קבצים, ומחליף בה רשימת טקסטים בטקסטים אחרים וכותב מחדש ל-HD.

ועכשיו נקרא את באנגלית ביחד:

Replace Text In Fiels( Texts To Replace with Text To Replace Them, URLs For Fiels To Replace Their Content

למישהו יש ספק קל ביותר מה המתודה הזו עושה או מה היא היא מקבלת כפרמטרים?

ככה נראה הקוד שלנו כרגע:  

 

        private static void ReplaceFilesContent()

        {

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            List<string> URLsForFilesToReplaceTheirContent = new List<string>();

            URLsForFilesToReplaceTheirContent.Add("myFile1.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile2.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile3.txt");

 

            ReplaceTextInFiels(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

 

        private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            //מילוי הנתונים

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

                {

                    string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                    Regex rg = new Regex(manf.OldName);

                    using (StreamReader sr = new StreamReader(path))

                    {

                        tochen = sr.ReadToEnd();

                    }

                    //החלפת הערכים

                    tochen = rg.Replace(tochen, manf.NewName);

                    //מחיקת הקובץ הישן

                    if (File.Exists(path))

                    {

                        File.Delete(path);

                    }

                    using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                    {

                        sw.Write(tochen);

                    }

                }

            }

        }

עכשיו נתאמץ ממש חזק להבין מה הולך פה - יש לולאה חיצונית שעוברת על כל הקבצים, ולולאה פנימית שעוברת על כל הביטויים להחלפה ומחליפה את התוכן שלה.

כבר אמרנו - אנחנו לא אוהבים הערות. נכניס כל לולאה למתודה נפרדת. הקוד הבא:

 

        private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            //מילוי הנתונים

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string tochen = "";

                foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

                {

                       ...

                }

            }

         }

הופך ל:

        private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

 

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string tochen = ""; foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

                {

                    string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                    Regex rg = new Regex(manf.OldName);

                    using (StreamReader sr = new StreamReader(path))

                    {

                        tochen = sr.ReadToEnd();

                    }

                    //החלפת הערכים

                    tochen = rg.Replace(tochen, manf.NewName);

                    //מחיקת הקובץ הישן

                    if (File.Exists(path))

                    {

                        File.Delete(path);

                    }

                    using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                    {

                        sw.Write(tochen);

                    }

                }

            }

        }

ואז ל:

      private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt>

TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

 

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string tochen = "";

                tochen = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, tochen);

            }

        }

 

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string tochen)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

            {

                   ....

            }

            return tochen;

        }

למעשה כתבנו כאן תיעוד - אמרנו שהלולאה הראשונה היא IterateOverAllFilesToReplaceTheirContent והלולאה השנייה היא ReplaceAllTextsInCurrentFile. הבהרנו למען קריאות בעתיד, למה בכלל כתבנו את הלולאה הזאת.

כתבנו למה בכלל יש כאן את הקוד הזה. מה הוא אמור לעשות, למה מבצעים אותו וכך הלאה.

 שימו לב אגב מה קיבלנו בפונקציה הראשונה:

        private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

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

התוכניתן שקורא למתודה הזו, בכלל לא יודע מה היא עושה, לא יודע כלום על לולאות, לא יודע כלום מה הולך בפנים - רק יודע מה הפונקציה עושה עבורו.

 

נעבור לשורת קוד הזו:

                string tochen = "";

לא כותבים פונטית אף-פעם.

בשפה שבה אתה כותב - זאת השפה שבאוצר מילים שלה אתה תשתמש.

אפשרות אחת (וזו שנדבוק בה כתכנתים) היא להשתמש רק באנגלית ולכן נשנה את שם המשתנה והפרמטרים ל-CurrentContentOfFile.

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string CurrentContentOfFile = "";

                CurrentContentOfFile = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, CurrentContentOfFile);

            }

        }

אפשרות נוספת, שלא נעבוד איתה היא לכתוב את שם הפרמטר בעברית.

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string תוכןהקובץ = "";

                תוכןהקובץ = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, תוכןהקובץ);

            }

        }

זה חוקי לחלוטין מבחינת הקומפיילר והקוד הזה יעבור קומפילציה.

אישית אני לא מעודד כתיבה בעברית ממספר סיבות.

הראשונה, עברית לא נתמכת טוב ב-Visual Studio או בשום Add-in. אורן עיני כתב על זה פוסט נהדר שמראה את המכשולים הטכניים לכתוב בעברית.

השנייה, זה הצורך הקבוע לעבור כל הזמן מעברית לאנגלית.

השלישית, עברית היא שפה נטולת Capital Letters וצריך להסתמך על קו תחתון _ בשבילה.

 

נחזור לשורה הזו

                string CurrentContentOfFile = "";

עדיף לאתחל פרמטר string עם String.Empty ולא עם גרשיים כפולות "".

                string CurrentContentOfFile = string.Empty;

 

נחזור למתודה שאנחנו כרגע עובדים עליה:

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt manf in TextsToReplaceWithTextToReplaceThem)

            {

                string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

                using (StreamReader sr = new StreamReader(path))

                {

                    CurrentContentOfFile = sr.ReadToEnd();

                }

                //החלפת הערכים

                Regex rg = new Regex(manf.OldName);

                CurrentContentOfFile = rg.Replace(CurrentContentOfFile, manf.NewName);

                //מחיקת הקובץ הישן

                if (File.Exists(path))

                {

                    File.Delete(path);

                }

                using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

                {

                    sw.Write(CurrentContentOfFile);

                }

            }

            return CurrentContentOfFile;

        }

 

מה אנחנו כבר יודעים שלא תקין כאן מבחינת קריאות?

 - השם פרמטר manf לא באמת אומר כלום על מה הפרמטר עושה ומה המטרה שלו. נשנה אותו ל-currentPairToBeUsedForReplacing.

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

 

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)

            {

                string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

 

                CurrentContentOfFile = GetContentFromFile(CurrentContentOfFile, path);

 

                CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);

 

                DeleteOldFile(path);

 

                WriteNewFile(CurrentContentOfFile, path);

            }

            return CurrentContentOfFile;

        }

 

        private static void WriteNewFile(string CurrentContentOfFile, string path)

        {

            using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

            {

                sw.Write(CurrentContentOfFile);

            }

        }

 

        private static void DeleteOldFile(string path)

        {

 

            if (File.Exists(path))

            {

                File.Delete(path);

            }

        }

 

        private static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)

        {

            Regex rg = new Regex(currentPairToBeUsedForReplacing.OldName);

            CurrentContentOfFile = rg.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.NewName);

            return CurrentContentOfFile;

        }

 

        private static string GetContentFromFile(string CurrentContentOfFile, string path)

        {

            using (StreamReader sr = new StreamReader(path))

            {

                CurrentContentOfFile = sr.ReadToEnd();

            }

            return CurrentContentOfFile;

        }

תזכרו שכל מה שאנחנו כתבנו מכל הקוד הזה - זה רק שמות המתודות במהלך Refactoring. כל תהליך שינוי הקוד בוצע אוטומטית עבורנו.  

שימו לב למתודה הראשית:

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)

            {

                string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

 

                CurrentContentOfFile = GetContentFromFile(CurrentContentOfFile, path);

 

                CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);

 

                DeleteOldFile(path);

 

                WriteNewFile(CurrentContentOfFile, path);

            }

            return CurrentContentOfFile;

        }

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

עבור כל זוג להחלפה:

1. תשיג תוכן הקובץ

2. תשנה את תוכן הקובץ

3. תמחק את הקובץ הישן

4. תכתוב את הקובץ עם התוכן החדש 

זה בדיוק מה שכתוב בקוד שלנו! שום הערות, שום תיעוד מסורבל, הכל מובע וכתוב בקוד שלנו.

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

 

ראינו כמה קונספטים מעניינים:

1. אחריות של מתודות - כל מתודה מבצעת דבר אחד ודבר אחד בלבד

2. הסתרה של אופן ביצוע - המתודה "הראשית" (ביחס לתהליך שלנו) לא חושפת מה בדיוק היא מבצעת.

 

נמשיך עם השורה הבאה:

                string path = HttpContext.Current.Server.MapPath("../../files/") + strFile;

נתנו למתודה ReplaceAllTextsInCurrentFile את האחריות לגלות את הנתיב המלא של הקובץ.

והרי אמרנו - אנחנו אוהבים שכל מתודה עושה דבר אחד ודבר אחד בלבד. אז נעביר את גילוי הנתיב המלא למתודה נפרדת.

                string path = GetPathForFile(strFile);

 

         ...

 

        private static string GetPathForFile(string strFile)

        {

            return HttpContext.Current.Server.MapPath("../../files/") + strFile;

        }

 

עכשיו נראה מתודה את המתודה הבאה:

        private static void WriteNewFile(string CurrentContentOfFile, string path)

        {

            using (StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.UTF8))

            {

                sw.Write(CurrentContentOfFile);

            }

        }

יש לנו בעיה עם הקוד הזה. בואו נראה את הקונסטרקטור (גם באנגלית: Contructor) של StreamWriter.

כרגע נגדיר שתמיד שנרצה לפתוח StreamWriter באפליקציה שלנו ולא רק במתודה הזו - אנחנו נרצה כברירת מחדל שהוא יהיה Append=true עם קידוד UTF8. שזה מצב מאוד הגיוני לאפליקציה שתומכת בעברית. במקום לאתחל ב-40 מקומות באפליקציה StreamWriter עם הפרמטרים האלו נרצה לרכז איפשהו אותם.

נרצה להגיד שכברירת מחדל באפליקציה שלנו עובדים עם append=true ו-encoding של UTF8.

נרצה גם שהמקום הזה שאומר את זה, יחזיר לנו מופע מאותחל של StreamWriter.

מסתבר שיש משהו שנקרא Factory Design Pattern שהמטרה שלו היא לענות בדיוק על מקרים כאלו.
ניצור מחלקה סטטית בצד שתהיה StreamWriterFactory ותחזיר לנו מופע חדש של StreamWriter. 

        private static void WriteNewFile(string CurrentContentOfFile, string path)

        {

            using (StreamWriter sw = SteamWriterFactory.CreateStreamWriter(path))

            {

                sw.Write(CurrentContentOfFile);

            }

        }

 

        public class SteamWriterFactory

        {

            public static StreamWriter CreateStreamWriter(string path)

            {

                return new StreamWriter(path, true, System.Text.Encoding.UTF8);

            }

        }

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

 

נביט על מתודה נוספת

        private static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)

        {

            Regex rg = new Regex(currentPairToBeUsedForReplacing.OldName);

            CurrentContentOfFile = rg.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.NewName);

            return CurrentContentOfFile;

        }

במתודה הזו, אנחנו משתמשים ב-PairOfTextToReplaceWithTextToReplaceIt כדי להחליט איזה טקסט מוחלף באיזה טקסט.

לי אישית, לא ברור ש-oldName זה הטקסט שמוחלף ו-newName זה הטקסט שמחליף אותו. נלך למחלקה שמכילה אותם ונביט עליה

        internal class PairOfTextToReplaceWithTextToReplaceIt

        {

            public PairOfTextToReplaceWithTextToReplaceIt(string newName, string oldName)

            {

                this.newName = newName;

                this.oldName = oldName;

            }

 

            private string newName;

            private string oldName;

 

            public string NewName

            {

                get

                {

                    return newName;

                }

            }

            public string OldName

            {

                get

                {

                    return oldName;

                }

            }

        }

נוכל באמצעות VS2005 לשנות את שמות המאפיינים (גם באנגלית: Properties) ו-VS ידאג עבורנו לשנות את שמות ה-Propert בכל מקום אחר.

 מחוץ למחלקה יש למאפיינים את השמות החדשים.

למרות זאת, בתוך המחלקה עדיין נישאר עם אותם שמות משתנים פנימיים וייתכן שנרצה לשנות אותם.

VS2005 לא תומך בזה, בשביל זה יש מוצרים צד שלישי כמו Resharper, כמו CodeSmart, כמו CodeItOnce ורבים נוספים.

נדגים שימוש ב-Resharper.

נשנה גם את NewName ל-TextToBeReplacedWith.

        internal class PairOfTextToReplaceWithTextToReplaceIt

        {

            public PairOfTextToReplaceWithTextToReplaceIt(string textToBeReplacedWith, string textToReplace)

            {

                this.textToBeReplacedWith = textToBeReplacedWith;

                this.textToReplace = textToReplace;

            }

 

            private string textToBeReplacedWith;

            private string textToReplace;

 

            public string TextToBeReplacedWith

            {

                get

                {

                    return textToBeReplacedWith;

                }

            }

            public string TextToReplace

            {

                get

                {

                    return textToReplace;

                }

            }

        }

וכמובן שבאמצעות VS2005 או באמצעות מוצרי צד-שלישי הקוד מחוץ למחלקה יראה אותו דבר:

        private static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)

        {

            Regex rg = new Regex(currentPairToBeUsedForReplacing.TextToReplace);

            CurrentContentOfFile = rg.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.TextToBeReplacedWith);

            return CurrentContentOfFile;

        }

ניקח עוד צעד אחד אחרון במתודה הזו ונשנה את rg לשם משמעותי כלשהו.

        private static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)

        {

            Regex regexUsedToReplaceText = new Regex(currentPairToBeUsedForReplacing.TextToReplace);

            CurrentContentOfFile = regexUsedToReplaceText.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.TextToBeReplacedWith);

            return CurrentContentOfFile;

        }

 

עכשיו בואו נראה דוגמה למה כל ה-Refactoring הזה טוב...

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

אפשר גם לראות את זה בקוד שלנו:

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string CurrentContentOfFile = string.Empty;

                CurrentContentOfFile = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, CurrentContentOfFile);

            }

        }

 

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)

            {

                string path = GetPathForFile(strFile);

 

                CurrentContentOfFile = GetContentFromFile(CurrentContentOfFile, path);

 

                CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);

 

                DeleteOldFile(path);

 

                WriteNewFile(CurrentContentOfFile, path);

            }

            return CurrentContentOfFile;

        }

אפשר לראות בעיינים וממש לקרוא שכל פעם שאנחנו מבצעים החלפה אנחנו קוראים את הקובץ מחדש, מוחקים את הישן וכותבים אחד חדש.

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

        private static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string path = GetPathForFile(strFile);

 

                string CurrentContentOfFile = GetContentFromFile(path);

 

                CurrentContentOfFile = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, CurrentContentOfFile);

 

                DeleteOldFile(path);

 

                WriteNewFile(CurrentContentOfFile, path);

            }

        }

 

        private static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)

            {

                CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);

            }

            return CurrentContentOfFile;

        }

באמצעות Refactoring גם ראינו את הבעיה בצורה הרבה יותר ברורה (כי בפעם הראשונה נחשף לענינו האלגוריתם שמימשנו) וגם היה לנו הרבה יותר קל לבצע שינויים.

 

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

        public static void Main()

        {

            ReplaceFilesContent();

        }

 

        private static void ReplaceFilesContent()

        {

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            List<string> URLsForFilesToReplaceTheirContent = new List<string>();

            URLsForFilesToReplaceTheirContent.Add("myFile1.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile2.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile3.txt");

 

            ReplaceTextInFiels(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

 

        private static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

אפשר להגדיר לכל מתודה כאן תפקיד - ReplaceFilesContent הוא זה שמבין איזה קבצים נחליף להם את התוכן ומה יהיו ההחלפות ו-ReplaceTextInFiels הוא התהליך שבפועל מבצע החלפות.

אבל שתי המתודות האלו יושבות באותה מחלקה. מבחינת אחריות ומבחינת שימוש חוזר בקוד נוציא החוצה את ReplaceTextInFiels שהוא התהליך עם כל המתודות הקשורות למחלקה נפרדת.

    public static class contentReplacer

    {

 

    }

נרצה להעביר את ReplaceTextInFiels למחלקה הזו. אבל אם נעביר רק אותה ויש עדיין מתודות פנימיות שהיא משתמשת מהמחלקה הקודמת, היא לא תתקמפל. נצטרך להעביר אותה וכל המתודות הקשורות.

VS2005 לא תומך בזה, ולכן אם נאלץ לעבוד איתו נצטרך להעביר ידנית מתודה-מתודה.

כלים צד-שלישי שציינתי קודם כן תומכים בתהליך. למשל באמצעות Resharper.

שואלים אותנו לאיפה אנחנו רוצים להעביר את המתודה (לאיזו מחלקה) ואיזה מתודות נרצה להעביר.
בכחול מסומן לנו המתודות שהמתודות שסימנו תלויות בהן וגם צריכות לעבור.

 

ובסופו של דבר הקוד שלנו יראה ככה:

    public class RefactoringExample

    {

        public static void Main()

        {

            ReplaceFilesContent();

        }

 

        private static void ReplaceFilesContent()

        {

            List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem = new List<PairOfTextToReplaceWithTextToReplaceIt>();

            TextsToReplaceWithTextToReplaceThem.Add(new PairOfTextToReplaceWithTextToReplaceIt("replace me", "with me"));

 

            List<string> URLsForFilesToReplaceTheirContent = new List<string>();

            URLsForFilesToReplaceTheirContent.Add("myFile1.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile2.txt");

            URLsForFilesToReplaceTheirContent.Add("myFile3.txt");

 

            ContentReplacer.ReplaceTextInFiels(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

    }

 

    public class PairOfTextToReplaceWithTextToReplaceIt

    {

        public PairOfTextToReplaceWithTextToReplaceIt(string textToBeReplacedWith, string textToReplace)

        {

            this.textToBeReplacedWith = textToBeReplacedWith;

            this.textToReplace = textToReplace;

        }

 

        private string textToBeReplacedWith;

        private string textToReplace;

 

        public string TextToBeReplacedWith

        {

            get

            {

                return textToBeReplacedWith;

            }

        }

        public string TextToReplace

        {

            get

            {

                return textToReplace;

            }

        }

    }

 

    public class SteamWriterFactory

    {

        public static StreamWriter CreateStreamWriter(string path)

        {

            return new StreamWriter(path, true, System.Text.Encoding.UTF8);

        }

    }

 

    public static class ContentReplacer

    {

        public static void ReplaceTextInFiels(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            IterateOverAllFilesToReplaceTheirContent(TextsToReplaceWithTextToReplaceThem, URLsForFilesToReplaceTheirContent);

        }

 

        public static void IterateOverAllFilesToReplaceTheirContent(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, List<string> URLsForFilesToReplaceTheirContent)

        {

            foreach (string strFile in URLsForFilesToReplaceTheirContent)

            {

                string path = GetPathForFile(strFile);

 

                string CurrentContentOfFile = GetContentFromFile(path);

 

                CurrentContentOfFile = ReplaceAllTextsInCurrentFile(TextsToReplaceWithTextToReplaceThem, strFile, CurrentContentOfFile);

 

                DeleteOldFile(path);

 

                WriteNewFile(CurrentContentOfFile, path);

            }

        }

 

        public static string ReplaceAllTextsInCurrentFile(List<PairOfTextToReplaceWithTextToReplaceIt> TextsToReplaceWithTextToReplaceThem, string strFile, string CurrentContentOfFile)

        {

            foreach (PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing in TextsToReplaceWithTextToReplaceThem)

            {

                CurrentContentOfFile = ReplaceFileContentsForCurrentTextToBeReplaced(CurrentContentOfFile, currentPairToBeUsedForReplacing);

            }

            return CurrentContentOfFile;

        }

 

        public static string GetPathForFile(string strFile)

        {

            return HttpContext.Current.Server.MapPath("../../files/") + strFile;

        }

 

        public static void WriteNewFile(string CurrentContentOfFile, string path)

        {

            using (StreamWriter sw = SteamWriterFactory.CreateStreamWriter(path))

            {

                sw.Write(CurrentContentOfFile);

            }

        }

 

        public static void DeleteOldFile(string path)

        {

            if (File.Exists(path))

            {

                File.Delete(path);

            }

        }

 

        public static string ReplaceFileContentsForCurrentTextToBeReplaced(string CurrentContentOfFile, PairOfTextToReplaceWithTextToReplaceIt currentPairToBeUsedForReplacing)

        {

            Regex regexUsedToReplaceText = new Regex(currentPairToBeUsedForReplacing.TextToReplace);

            CurrentContentOfFile = regexUsedToReplaceText.Replace(CurrentContentOfFile, currentPairToBeUsedForReplacing.TextToBeReplacedWith);

            return CurrentContentOfFile;

        }

 

        public static string GetContentFromFile(string path)

        {

            string CurrentContentOfFile;

            using (StreamReader sr = new StreamReader(path))

            {

                CurrentContentOfFile = sr.ReadToEnd();

            }

            return CurrentContentOfFile;

        }

    }

 

הקוד שקיבלנו הוא:

1. הרבה יותר קריא

2. הרבה יותר ברור מה המטרה של הקוד כ-כלל

3. הרבה יותר ברור מה המטרה של כל שורת קוד פונקציונלית שכתבנו

4. יותר קל לשנות אותו ולעשות שינויים של האלגוריתם וגם באופן הביצוע הפנימי שלו

 

את הקוד הזה יכול לקרוא תוכניתן שלא מבין כלום במה עושה המערכת. זה קוד בר-תחזוקה.

 

נעבור בקצרה על מה ראינו:

1.  נתינת שמות משמעותיים למחלקות שמכילות מידע. איך לשנות את השם שלהן באמצעות VS2005.

2. נתינת שמות משמעותיים ל-Properties של מחלקות. איך לשנות את השם של Propert באמצעות VS2005, ואיך מוצרים צד-שלישי מאפשרים שינוי מקיף של כל המחלקה ולא רק שם ה-Property.

3. ברוב המוחץ של המקרים נעדיף להימנע מלכתוב הערות ונעדיף קוד שמתעד את עצמו.

4. נתינת שמות משמעותיים למשתנים פנימיים.

5. חלוקת אחריות בין מחלקות ומתודות שונות.

6. ההטבות ש-Refoactoring מביא לנו מבחינת קריאות ויכולת תחזוקת ושינוי הקוד.

7. פתרון בעיות נפוצות באמצעות Design Patterns.

8. אין שכפול קוד ברחבי המערכת.

 

לקוד הזה יש עוד כברת דרך לעבור. גם מבחינת Refactoring וגם היות שהוא עדיין לא Testabily. עדיין אי-אפשר לכתוב קוד שבודק את הקוד הזה באמצעות Unit Testing היות והוא ניגש ישירות ל-HD מה שהופך אותו ל-Intergration Testing. אז נדרש שם עוד שינוי ארכיטקטורי בשביל זה. אבל טסטביליות זה נושא נפרד מ-Refactoring וזה ליום אחר.

 

קישור: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=100306278

Removing Script# (ScriptSharp) Item Template

If like me you like to compile your C# code to JavaScript you probably use Script#.

And if like me you use your Visual Studio 2005 for something other then Script# programming you must know - Script# Item templates are the worst thing to ever happen to the human race since pigeons.

Let me explain. Script# Installs an Item Template. Like the "Windows forms" item template or "config file" item template. The problem is Script#'s Item template sits in it's own folder which somehow becomes the default folder when creating ANY new item.

Let's say I've got a Windows Forms project and I want to add a new WinForm:

When I click the magical "Windows Form" button on the menu one would expect I would be able to create a new Windows Form Item. This is what you get if you have Script# Installed:

Apparently Script# is so important it has it's own folder.
And having your own folder means - you're the default Item.

This is how you add Script# to the normal Item templates:

1. Close VS2005

2. Go to %program files%\Microsoft Visual Studio 8\Common7\IDE\ItemTemplates\CSharp

3. copy ScriptClass.zip from "Script#" directory to the main directory. (or delete it all togather if you don't think you don't even need it)

4. delete the "Script#" directory

5. open "Visual Studio 2005 Command prompt" (Start --> Programs --> Microsoft Visual Studio 2005 --> Microsoft Visual Studio Tools --> Visual Studio 2005 Command prompt)

6. run "devenv /setup" to rebuild Visual Studio template cache.

7. rejoice for you no longer have the Script# Item Template in a seperate directory.

 

Blog now FireFox compatible

FireFox just doesn't play nice with CSS pseudo-selector blocks.

I use the following CSS code in order to highlight & color the first letter in every paragraph:

.post p:first-letter { color: #1084FF; font-weight: bold; }

The end result in Internet explorer and the correct rendering is as such:

 

However this how it's rendered in FireFox 2.0:

Apparently mixing Hebrew & English with "p:first-letter" makes FireFox go nutty.

After checking live-search for a solution and seeing there is none I've decided that I'll disable this feature for FireFox users.

The following CSS:

.post p:first-letter { color: #1084FF; font-weight: bold; }

becomes:

.post *p:first-letter { color: #1084FF; font-weight: bold; }

 the * symbol is used to specify that only IE should render this CSS.

Question from Tapuz .Net forum: Returning an Enum value and name from Microsoft AJAX WebServices (Creating Custom JavaScriptConverter, Javascript Enums)

שאלה:

 אני עובדת עם Microsoft AJAX ומחזירה מהשרת ללקוח Enumים. הבעיה היא שבמקום להעביר ללקוח את הטקסט של ה-Enum אנחנו מקבלים את האינדקס של הערך הנבחר.

אפשר לשנות את ההתנהגות הזו שיעביר את הטקסט של הערך? אולי בצמוד לאינדקס?

 

תשובה:

 בואו נראה דוגמה לבעיה לפני שנפתור אותה.

נתקין Microsoft AJAX ו-ASP.Net Futures אחרי שהורדנו אותם מכאן - http://ajax.asp.net/downloads/default.aspx?tabid=47.

 ניצור אתר Microsoft AJAX CTP Enabled חדש.

נכתוב Enum חדש בדוט נט.

    public enum myEnum

    {

        First,

        Second

    }

ניצור דף בסיסי שמשתמש ב-PageMethods כדי להחזיר ערכים ללקוח.

 <body onload="UseEnum()">

    <form id="form1" runat="server">

        <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />

        <script type="text/javascript">

        function UseEnum()

        {

            PageMethods.ReturnMyEnum(onReturnMyEnum);

        }

 

        function onReturnMyEnum(result)

        {

            alert(result);

        }           

        </script>

    </form>

יש לנו ScriptManager בדף עם EnablePageMethods=True כדי שנוכל לקרוא למתודות public static שיושבות ב-code behind של הדף מתוך ג'אווה סקריפט.
שהטופס עולה אנחנו קוראים לפונקציה בג'אווה סקריפט בשם UseEnum שמריצה מתודה (גם באנגלית: Method) צד-שרת בשם ReturnMyEnum והערך שהמתודה מחזירה מודפס באמצעות alert.

נראה את ReturnMyEnum.

public partial class Default3 : System.Web.UI.Page

{

    [WebMethod()]

    public static myEnum ReturnMyEnum()

    {

        return myEnum.First;

    }

}

נריץ את הדף ומה שנקבל זה:

לא שימושי מדי, הרי בצד לקוח - אין לאפס הזה שום משמעות עבורנו.

נתחיל מהפתרון הפשוט והלוקאלי ביותר.
אם רק רצינו להחזיר את הטקסט "First" פשוט נשנה את ערך ההחזרה באמצעות ToString.

    [WebMethod()]

    public static string ReturnMyEnum()

    {

        return myEnum.First.ToString();

    }

ושנריץ את הדף נקבל:

יש עם הפתרון הזה כמה בעיות:

1. כמות השינוי שנדרשת באפליקציה שלנו. ברור ששינוי בצד-לקוח ידרש לא משנה מה יהיה הפתרון שלנו, אבל עם הפתרון הזה נצטרך לערוך את כל המתודות צד-שרת שיחזירו string ולא-enum. זה לא מעט עבודה בפרוייקט קיים.

2. המילה "First" לא מייצגת שום דבר, היא רק מחרוזת. היא לא חלק מאיזה Enum גדול יותר.

 

פתרון נוסף, יפתור את הבעיה המקורית ובעיה מספר 2 שהעלנו.
נמשיך להחזיר ללקוח את האינדקס הנבחר של ה-Enum, אבל נשתמש ב-Microsoft AJAX Javascript Enhancments כדי להגדיר את ה-Enum בצד-לקוח.

מה מסתבר? Microsoft AJAX לא רק מביא לנו אפשרויות תקשורת מעניינות, אלא גם הוא הרחיב את שפת ג'אווה סקריפט.
באמצעות Microsoft AJAX ניתן להגדיר Enumים בצד-לקוח בג'אווה סקריפט.

אם זו ההגדרה של myEnum ב-#C

    public enum myEnum

    {

        First,

        Second

    }

אז זו תהיה ההגדרה של myEnum בג'אווה סקריפט

        myEnum = function() {}

        myEnum.prototype =

        {

            First : 0,

            Second : 1

        }

        myEnum.registerEnum("myEnum");   

נחזור ונדגיש שחזרנו לעבוד עם החזרה רגילה של Enumים (שלמעשה מחזירים את האינדקס הנבחר ב-Enum).

    [WebMethod()]

    public static myEnum ReturnMyEnum()

    {

        return myEnum.First;

    }

מפאת ש-myEnum מוגדר עכשיו בלקוח, נוכל לקחת את האינדקס הנבחר שקיבלנו בלקוח ולקבל בחזרה Enum בצד-לקוח.

        function onReturnMyEnum(result)

        {

            alert( result + " " + myEnum.toString(result) );

        }

שנריץ נקבל:

 

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

מה הבעיה עכשיו? יש לנו שכפול קוד בין הצד-שרת ב-#C לבין הצד-לקוח בג'אווה סקריפט.
הרי ההגדרה של myEnum למעשה חוזרת על עצמה, פעם בשרת ופעם בלקוח. ואם נשנה את הצד-שרת ייתכן וכי ההתנהגות צד-לקוח תישבר.

אם נרצה נוכל לנפתור את הבעיה הזו באמצעות #Script, פריימוורק צד-שלישי שמקמפל #C לג'אווה סקריפט. אפשר להוסיף ל-Build משימה שמקמפלת את ה-Enum צד-שרת שלנו ל-Enum צד-לקוח.

 

נחזור לבעיה שיש לנו עם הפתרון המקורי של שימוש במחרוזת ו-ToString.
אם נחליט על האופציה הזו, נאלץ לשנות את כל הקוד צד-שרת שלנו שבאמת יחזיר רק מחרוזות.

הייתי מעדיף פתרון יותר כוללני כאן, משהו שיאפשר לי להמשיך להחזיר Enumים כרגיל וייתן לי כמה שיותר מידע על ה-Enum בצד-לקוח.

בואו נביט על משהו מעניין בקובץ ה-web.config שלנו (שיופיע רק ב-CTP Enabled Microsoft AJAX Website).

    <system.web.extensions>

        <scripting>

            <webServices>

                <jsonSerialization maxJsonLength="500">

                    <converters>

                        <add name="DataSetConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataSetConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                        <add name="DataRowConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataRowConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                        <add name="DataTableConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataTableConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                    </converters>

                </jsonSerialization>

            </webServices>

        </scripting>

    </system.web.extensions>

יש כאן משהו מאוד מעניין, מסתבר שכחלק מה-ASP.Net Futures אפשר להחזיר מצד-שרת לצד-לקוח דברים כמו DataSet ו-DataTable והם אוטומטית יומרו לאיזה חיה מוזרה בשם JSON. אנחנו לא מכירים את JSON וגם לא נדבר על הטכנלוגיה הזו יותר מדי, אבל זה כנראה מאוד איזה פורמט תקשורת צד-לקוח צד-שרת מעניין.

מה השלוש שורות האלו של <converters> ב-web.config אומרות לנו?
שכדי להחזיר מצד-שרת DataSetים ו-DataTables אנחנו רושמים משהו שנקרא Converter והוא דואג להמיר אותם לפורמט שניתן לעבוד איתו בלקוח.

אז בואו גם אנחנו ניצור Converter משלנו. לפני זה נחזור ונזכיר שאנחנו מחזירים כרגיל מהצד-שרת Enum:

    [WebMethod()]

    public static myEnum ReturnMyEnum()

    {

        return myEnum.First;

    }

ניצור מחלקה משלנו שהתפקיד שלה יהיה לקחת Enum כלשהו (לא בהכרח myEnum) ולהמיר אותו לפורמט שנקבל בצד-לקוח Value ו-Index.

נתחיל מלרשת את המחלקה האבסטרקטית JavaScriptConverter.

    public class EnumConverter : System.Web.Script.Serialization.JavaScriptConverter

    {

        public override IEnumerable<Type> SupportedTypes

        {

            get { throw new Exception("The method or operation is not implemented."); }

        }

 

        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)

        {

            throw new Exception("The method or operation is not implemented.");

        }

 

        public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)

        {

            throw new Exception("The method or operation is not implemented.");

        }

    }

המחלקה הזו דורשת מאתנו לממש שלושה דברים:

1.  SupportedTypes שמחזיר רשימה של טיפוסים בהם ה-Converter הספציפי הזה מטפל

2.  Serialize שמקבל אלמנט ומחזיר רשימת ערכים שיהיו זמינים על האובייקט צד-לקוח

3. Deserialize שמקבל ערכים על אלמנט-צד-לקוח והופך אותו בחזרה להמחלקה

 

במקרה שלנו אמרנו ש-EnumConverter יתמוך בכל Enum, אז נדאג להצהיר על כך ב-SupportedTypes.

        public override IEnumerable<Type> SupportedTypes

        {

            get

            {

                return new ReadOnlyCollection<Type>(new Type[] { typeof(Enum) });

            }

        }

נמשיך בלממש את Serialize.

        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)

        {

            Enum curEnum = obj as Enum;

            if (curEnum == null)

                throw new ArgumentException("only Enum must be sent to EnumConverter");

 

        }

אמרנו הרי שאנחנו תמיד נטפל ב-Enumים, אז נמיר את האובייקט שקיבלנו ל-Enum ולמקרה שקיבלנו משהו שהוא לא Enum נזרוק חריגה.

עכשיו מגיע החלק הבאמת מעניין.

        public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)

        {

            Enum curEnum = obj as Enum;

            if (curEnum == null)

                throw new ArgumentException("only Enum must be sent to EnumConverter");

 

            IDictionary<string, object> ReturnValues = new Dictionary<string, object>();

            ReturnValues.Add("name", obj.ToString());

            ReturnValues.Add("value", Convert.ToInt32(obj));

 

            return ReturnValues;

        }

אמרנו ככה - כל Enum שנקבל, נחזיר את הטקסט שלו במאפיין (גם באנגלית: Property) בשם name ואת האינדקס שלו בתוך value.
עוד שנייה גם נראה איך ניגש למאפיינים האלו בצד-לקוח.

את Deserialize לא נממש כי לפחות בדוגמה כרגע לא נשלח בחזרה לשרת ערכים שה-Converter הזה אמור לטפל בהם.

נרשום את ה-Converter ב-web.config שלנו. (בהנחה והוא יושב ב-App_code)

                <jsonSerialization maxJsonLength="500">

                    <converters>

                        <add name="EnumConverter" type="myConverters.EnumConverter"/>

                        <add name="DataSetConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataSetConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                        <add name="DataRowConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataRowConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                        <add name="DataTableConverter" type="Microsoft.Web.Preview.Script.Serialization.Converters.DataTableConverter, Microsoft.Web.Preview, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

                    </converters>

                </jsonSerialization>

ולמה עשינו את כל העבודה הזו? כדי שנוכל לגשת למאפיינים name ו-value בצד לקוח בג'אווה סקריפט.

        <script type="text/javascript">

        function UseEnum()

        {

            PageMethods.ReturnMyEnum(onReturnMyEnum);

        }

 

        function onReturnMyEnum(result)

        {

          alert( result.value + " " + result.name);

        }         

        </script>

ואם נריץ עכשיו את הדף נקבל:

 

הקוד שעבדנו עליו זמין להורדה כאן - http://www.JustinAngel.Net/files/Example-AjaxEnum.zip

Question from .Net Tapuz forum: Winforms WebBrowser invoking Javascript and Javascript invoking Winforms

שאלה:

 יש לנו טופס Winforms שפותח דף Web (שהוא HTML קלאסי) שאנחנו פיתחנו.

היינו רוצים שה-Javascript בדף HTML יוכל להעביר מסרים לטופס Winforms ושזה יגיב.

אנחנו גם צריכים שה-Winforms יעביר ל-Javascript נתונים נוספים.

יש כלי מובנה ב-Framework לזה?

 

תשובה:

 נחדד את נושא השאלה: תקשורת Winforms בין Javascript.
לתקשורת הזו יש שני כיוונים: Javascript שמדבר עם Winforms והכיוון השני הוא Winforms שמדבר עם Javascript.

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

בנוסף, ניצור קובץ HTML בסיסי.

<html>

<body>

    <h1> Hello world! </h1>

</body>

</html>

נשנה את ה-URL של ה-WebBrowser שלנו שיצביע לקובץ HTML שלנו.

נריץ את האפליקציה המפוארת שבנינו וקיבלנו:

מדהים.

עכשיו נהפוך את הדוגמה הזו מ"גן ילדים" לאפליקציה של העולם האמיתי.

נרצה שבדף HTML שלנו יהיה רשימה של כוננים קשיחים (Hard Drives)  אפשריים במחשב, בלחיצה על כונן מסויים, הדף HTML יתמלא ברשימת הקבצים והתיקיות בספרייה הראשית של הכונן.

בואו גם נחשוב ביחד על למה לא מספיק כאן ג'אווה סקריפט.
ג'אווה סקריפט רץ דרך Sand-Box של הדפדפן ולמרות שיש לו יכולות אי-אלו ויכולות גרפיות מרשימות, הדפדפן מוגבל מבחינת אבטחה.
למרות שאפשר לעשות הרבה עם ג'אווה סקריפט, כיווני הברירת מחדל של הדפדפנים מונעים דברים כמו הרצת תוכניות על מחשב הלקוח, סריקת הקבצים על מחשב הלקוח, גישה ישירה למדפסת וכך הלאה. 
אז נאלץ לחזור לאפליקציית Winforms שאם תרוץ עם ההרשאות המתאימות תוכל כן לבצע דברים אלו. 

הנה דף ה-HTML שלנו:

<html>

<body>

    <h1> Select a drive to list it's files: </h1><br />

    <span style="text-decoration:underline; cursor: pointer;">C:\</span><br />

    <span style="text-decoration:underline; cursor: pointer;">D:\</span><br />

    <span style="text-decoration:underline; cursor: pointer;">E:\</span><br />

</body>

</html>

סה"כ span עם קצת עיצוב שיראה כמו קישור. וככה זה נראה.

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

מסתבר שעל ה-WebBrowser יש מאפיין (גם באנגלית: Property) מעניין בשם WebBrowser.ObjectForScripting.
המאפיין המוזר הזה מאפשר לנו לגשת מתוך הדפדפן לממשק חלונאי.

נגדיר מתודה (גם באנגלית: Method) בשם myMethod על האלמנט החלונאי שמכוון ל-ObjectForScripting, ודרך גישה ל-window.external.myMethod בג'אווה סקריפט נוכל לקרוא למתודה הזו.

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

        private void Form16_Load(object sender, EventArgs e)

        {

            webForScripting.ObjectForScripting = this;

        }

בנוסף, אותו אלמנט חלונאי חייב להיות מסומן עם ComVisiableAttribute.

    [System.Runtime.InteropServices.ComVisibleAttribute(true)]

    public partial class Form16 : Form

    {

        public Form16()

        {

            InitializeComponent();

        }

 

        private void Form16_Load(object sender, EventArgs e)

        {

            webForScripting.ObjectForScripting = this;

        }

    }

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

        public void ListFilesOnHardDrive(string DriveLetter)

        {

            DriveInfo driveToIterateOverItsRootFiles = new DriveInfo(DriveLetter);

            foreach (FileInfo curFileToPrint in driveToIterateOverItsRootFiles.RootDirectory.GetFiles())

            {

                tbxResults.AppendText(curFileToPrint.Name + "\r\n");

            }

        }

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

אם למשל היינו רוצים לקרוא למתודה הזו מתוך קוד דוט-נטי היינו עושים את זה ככה:

        private void Form16_Load(object sender, EventArgs e)

        {

            ListFilesOnHardDrive("C");

        }

וזה היה נראה ככה.

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

<html>

<body>

    <h1> Select a drive to list it's files: </h1><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('C');">C:\</span><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('D');">D:\</span><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('E');">E:\</span><br />

</body>

</html>

 השתמשנו ב-window.external וקראנו למתודה דוט-נטית לחלוטין!

עכשיו נרצה במקום להדפיס לתיבת טקסט דוט-נטית את רשימת הקבצים, נרצה להדפיס אותה בדף ה-HTML שלנו.

נוסיף מתודת ג'אווה סקריפט שמקבלת שם קובץ ומדפיסה אותו לתוך תגית <div> עם רשימת הקבצים.

 

<html>

<head>

<script type="text/javascript">

function PrintFileName(fileName)

{

    document.getElementById("divResults").innerHTML  += fileName + "<br />";

}

</script>

</head>

<body>

    <h1> Select a drive to list it's files: </h1><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('C');">C:\</span><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('D');">D:\</span><br />

 

    <span style="text-decoration:underline; cursor: pointer;"

            onclick="window.external.ListFilesOnHardDrive('E');">E:\</span><br />

 

    <h1> Files:</h1>

    <div id="divResults" />

</body>

</html>

אם היינו רוצים לקרוא למתודת ג'אווה סקריפט הזו מתוך ה-HTML היינו עושים משהו כזה:

    <body onload="PrintFileName('myFile.ext')">

אז עכשיו נשאלת השאלה איך נקרא למתודת ג'אווה סקריפט הזו מתוך הטופס החלונאי שלנו?

נשתמש ב-WebBrowser.Document.InvokeScript שמקבל את שם הפונקציה בג'אווה-סקריפט שנרצה להריץ ומערך ערכים שנשלח לפונקציה. נשכתב את ListFilesOnHardDrive.

        public void ListFilesOnHardDrive(string DriveLetter)

        {

            DriveInfo driveToIterateOverItsRootFiles = new DriveInfo(DriveLetter);

            foreach (FileInfo curFileToPrint in driveToIterateOverItsRootFiles.RootDirectory.GetFiles())

            {

                webForScripting.Document.InvokeScript("PrintFileName", new string[] {curFileToPrint.Name});

                //tbxResults.AppendText(curFileToPrint.Name + "\r\n");

            }

        }

נריץ את האפליקציה ונבחר את כונן C.

אז מה יש לנו כאן?

קוד ג'אווה סקריפט שקורא למתודה בדוט-נט,
וקוד בדוט-נט שקורא לקוד ג'אווה סקריפט.

 

הקוד שעבדנו עליו זמין להורדה כאן - http://www.JustinAngel.Net/files/Blog-JavaScriptWinformsInterop.zip.

Question from Tapuz .Net forum: Exposing a Read-only collection from a class

שאלה:

בנינו מחלקה שמכילה כמה סוגים של Collections ואנחנו חושפים אותם החוצה ב-Properties.

ה-Collections האלו הם Private למחלקה עצמה, והחוצה אנחנו רוצים לחשוף רק אפשרויות לעבור עליהם בקריאה בלבד.

ההוספה והמחיקה לאוספים הפנימיים הוא תוצאה של אלגוריתמים פנימיים, למשל אנחנו רוצים שיהיה אפשר להוסיף רק דרך Add שלנו כדי שנוכל לעלות Events.

 הבעיה היא שב-Get של ה-Property אנחנו מחזירים Reference Type ואפשר לשנות מחוץ למחלקה את ה-Collections בלי לעבור דרך האלגורתימים.

יש לזה פתרון בדוט נט?

 

תשובה:

בואו נבנה מחלקה לדוגמה שחושפת שלושה סוגי אוספים: (גם באנגלית: Collections)

1. ArrayList - שהשתמנו בדוט נט 1.1

2. List ג'נארי של דוט נט 2.0

3. StringCollection  - שזה Collection יעודי שעבר אופטיזמציה לטפל רק במחרוזות

ככה נראית המחלקה:

    public class myClassHasCollections

    {

        private ArrayList _dotNet11Collection = new ArrayList();

        public ArrayList DotNet11Collection

        {

            get { return _dotNet11Collection; }

        }

 

        private List<SomeClass> _dotNet20Collection = new List<SomeClass>();

        public List<SomeClass> DotNet20Collection

        {

            get { return _dotNet20Collection; }

        }

 

        private StringCollection _dotNet20SpeaclizedStringCollection = new StringCollection();

        public StringCollection DotNet20SpeaclizedStringCollection

        {

            get { return _dotNet20SpeaclizedStringCollection; }

        }

    }

 אפשר לראות שיש אוספים שהם Private בתוך המחלקה.

למשל, נוכל ליצור מתודות (גם באנגלית: Methods) שחושפות מחוץ למחלקה פונקציונליות של הוספה, מחיקה ועדכון ורק דרכן נרצה שיהיה אפשר לשנות את האוספים.

    public class myClassHasCollections

    {

        private ArrayList _dotNet11Collection = new ArrayList();

        public ArrayList DotNet11Collection

        {

            get { return _dotNet11Collection; }

        }

 

        public void AddToDotNet11Collection(object objToAdd)

        {

            _dotNet11Collection.Add(objToAdd);

        }

 

 

        private List<SomeClass> _dotNet20Collection = new List<SomeClass>();

        public List<SomeClass> DotNet20Collection

        {

            get { return _dotNet20Collection; }

        }

 

        public void AddToDotNet20Collection(SomeClass someClassToAdd)

        {

            _dotNet20Collection.Add(someClassToAdd);

        }

 

 

        private StringCollection _dotNet20SpeaclizedStringCollection = new StringCollection();

        public StringCollection DotNet20SpeaclizedStringCollection

        {

            get { return _dotNet20SpeaclizedStringCollection; }

        }

 

        public void AddToDotNet20SpeaclizedStringCollection(string StringToAdd)

        {

            _dotNet20SpeaclizedStringCollection.Add(StringToAdd);

        }

    }

  

למשל ככה היינו ניגשים למחלקה:

            myClassHasCollections collections = new myClassHasCollections();

 

            collections.AddToDotNet11Collection(new object());

            collections.AddToDotNet20Collection(new SomeClass());

            collections.AddToDotNet20SpeaclizedStringCollection(www.JustinAngel.Net);

 

ואם היינו רוצים למשל לעבור על ה-List הגנ'ארי היינו עושים:

            foreach (SomeClass someClass in collections.DotNet20Collection)

            {

                Console.WriteLine(someClass.Key);

            }

 

אז מה הבעיה עם המחלקה בצורה הנוכחית? 
אנחנו יכולים לשנות את האוסף כי קיבלנו אותו Reffernce Type.

בואו נביט על הקוד הבא:

            collections.DotNet11Collection.Add(new object());

            collections.DotNet20Collection.Add(new SomeClass());

            collections.DotNet20SpeaclizedStringCollection.Add("www.JustinAngel.Net");

אנחנו ניגשים ישירות למתודות שחושף כל אוסף ואוסף ומשנים אותו. למרות שרצינו לחשוף אותם כקריאה בלבד.

 

מה מסתבר? עם הפריימוורק מגיעים סוגי אוספים שהם קריאה בלבד.
האוספים האלו הם עותקים שרק אפשר לעבור להם על התוכן, אבל לא ניתן להוסיף, למחוק או בכלל לשנות אותם.

נשנה את המאפיינים שלנו שיחזירו אוספים לקריאה בלבד.

 

על List ג'נארי יושבת מתודה בשם List<T>.AsReadOnly שמחזירה אוסף לקריאה בלבד מסוג ReadOnlyCollection ג'נארי.

במקום להחזיר List ג'נארי נחזיר ReadOnlyCollection ג'נארי.

        private List<SomeClass> _dotNet20Collection = new List<SomeClass>();

        public List<SomeClass> DotNet20Collection

        {

            get { return _dotNet20Collection; }

        }

הופך ל:

        private List<SomeClass> _dotNet20Collection = new List<SomeClass>();

        public ReadOnlyCollection<SomeClass> DotNet20Collection

        {

            get { return _dotNet20Collection.AsReadOnly(); }

        }

 

        public void AddToDotNet20Collection(SomeClass someClassToAdd)

        {

            _dotNet20Collection.Add(someClassToAdd);

        }

 

ועכשיו נוכל להוסיף רק דרך המתודת Add שלנו.

 

ב-ArrayList יש מתודה סטטית בשם ArrayList.ReadOnly שמקבלת ArrayList ומחזירה ArrayList שהמימוש הפנימי שלו יזרוק שגיאה כאשר ינסו לשנות אותו. נדאג להחזיר רק ArrayList שהוא קריאה בלבד מהמאפיינים.

        private ArrayList _dotNet11Collection = new ArrayList();

        public ArrayList DotNet11Collection

        {

            get

            {

                return ArrayList.ReadOnly(_dotNet11Collection);

            }

        }

ואם ננסה להוסיף לאוסף ישירות בלי לעבור דרך מתודת ה-Add נקבל שגיאת זמן ריצה.

            collections.DotNet11Collection.Add(new object());

 

בנושא ה-StringCollection אנחנו קצת בבעיה כי אין לנו שום טיפוס מקביל ל-StringCollection שהוא קריאה בלבד.
יש לנו שתי אפשרויות: הראשונה היא להמיר אותו ל-List ג'נארי ולהחזיר ReadOnlyCollection ג'נארי.
האפשרות השנייה היא להשתמש ב-ArrayList.ReadOnly. מה מסתבר? ה-ArrayList.ReadOnly גם יכול לקבל סתם IList כללי ויחזיר IList שהוא קריאה בלבד.

        private StringCollection _dotNet20SpeaclizedStringCollection = new StringCollection();

        public StringCollection DotNet20SpeaclizedStringCollection

        {

            get { return _dotNet20SpeaclizedStringCollection; }

        }

הופך ל:

        private StringCollection _dotNet20SpeaclizedStringCollection = new StringCollection();

        public IList DotNet20SpeaclizedStringCollection

        {

            get

            {

                return ArrayList.ReadOnly(_dotNet20SpeaclizedStringCollection);

            }

        }

 

        public void AddToDotNet20SpeaclizedStringCollection(string StringToAdd)

        {

            _dotNet20SpeaclizedStringCollection.Add(StringToAdd);

        }

ואם ננסה להריץ את הקוד הבא נקבל שגיאת זמן ריצה.

            collections.DotNet20SpeaclizedStringCollection.Add(www.JustinAngel.Net);

An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll

Additional information: Collection is read-only.

 

בזמן ריצה נוכל לבדוק לכל אחד מהאוספים הללו IsReadOnly כדי לראות אם האוסף שבידינו הוא עותק לקריאה בלבד.

 

Question from .Net Tapuz forum: Drawing with a mouse/Stylus in Windows Forms 2.0 (not using GDI+)

איך אפשר להשתמש בעכבר או בעט סטיילוס (במחשבי Tablet) כדי לצייר ב-Windows Forms?

ציור על המסך באמצעות עזרים חיצוניים (עכבר במקרה של מחשבים שולחניים או ציור באמצעות לחיצות על המסך במקרה של Tablet Pc) הוא מנת חלקם של מחשבי Tablet.

יש DLL דוט-נטי בשם Microsoft.Ink.DLL שבא עם ההתקנה של Tablet PC SDK.
כחלק מה-DLL המסתורי הזה, מגיעים כמה מחלקות מעניינות מאוד שמאפשרות לנו "לצייר על פקדים".

נוריד את ה-SDK מכאן - http://www.microsoft.com/downloads/details.aspx?FamilyId=B46D4B83-A821-40BC-AA85-C9EE3D6E9699&displaylang=en.

נפתח פרוייקט WinForms חדש.

כדי להשתמש ב-DLL נוסיף Refference חדש לפרוייקט ל - Microsoft Tablet PC API.

עכשיו, נוסיף לטופס שלנו Panel רגיל לחלוטין שעליו החלטתי שנצייר.

נשתמש ב-Microsoft.Ink.InkOverlay.
InkOverlay הוא בדיוק כמו שהוא נשמע - מאפשר ציור מעל פקד גרפי מסויים.
ניצור מופע של InkOverlay ברמת הטופס, נקבע שהוא עובד על ה-Panel שלנו ובנוסף לדאוג לבצע Dispose שלו בסגירת הטופס כדי להימנע מזליגות זכרון.

 

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

 

using Microsoft.Ink;

 

namespace WinTesting

{

    public partial class Form15 : Form

    {

        public Form15()

        {

            InitializeComponent();

        }

 

        private InkOverlay panelOverlay;

        private void Form15_Load(object sender, EventArgs e)

        {

            panelOverlay = new InkOverlay(this.panel1);

            panelOverlay.Enabled = true;

        }

 

        private void Form15_FormClosing(object sender, FormClosingEventArgs e)

        {

            panelOverlay.Dispose();

        }

    }

}

מאוד פשוט יחסית, איתחלנו מופע של המחלקה שמכוון על ה-Panel וכל השאר זה מסביב (להתחיל את ה-InkOverlay, להיפטר ממנו, להוסיף Using).

בואו נראה את הטופס שלנו אחרי שציירתי עליו עם העכבר:

(אכן מסמך אנושי מזעזע)

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

נכיר מחלקה נוספת בשם DrawingAttributes. המחלקה הזו מכילה סט של מאפיינים (גם באנגלית: Properties) שקובעים את "העט" איתו אנו מציירים.
על InkOverlay יושב מאפיין בשם DefaultDrawingAttributes מסוג DrawingAttributes שדרכו נקבע את העט איתו אנו מציירים.

נרצה לשנות כמה פרטים מעניינים של העט שלנו דרך ה-GUI: הרוחב שלו, ה-Transperncy, צבע המברוש והאם הציור הוא Anti-Aliased.

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

נחבר עכשיו את האירועים שלהם לשינויים של ה-Drawing Attributes.

        private void cbxAntiAlias_CheckedChanged(object sender, EventArgs e)

        {

            panelOverlay.DefaultDrawingAttributes.AntiAliased = cbxAntiAlias.Checked;

        }

 

        private void numWidth_ValueChanged(object sender, EventArgs e)

        {

            panelOverlay.DefaultDrawingAttributes.Width = float.Parse(numWidth.Value.ToString());

        }

 

        private void numTransperncy_ValueChanged(object sender, EventArgs e)

        {

            panelOverlay.DefaultDrawingAttributes.Transparency = byte.Parse(numTransperncy.Value.ToString());

        }

התחברנו לאירועים השונים של התערבות משתמש ב-GUI (לחיצה על ה-CheckBox או שינוי ערך ב-NumericUpDown) ושנינו את הערך הרלוונטי שלהם ב-DrawingAttributes לערך הנוכחי בפקד.

נוסיף גם טיפול בשינוי צבע באמצעות ה-ColorDialog שהוא דיאלוג שמאפשר למשתמש לבחור צבע מתוך הדיאלוג הסטנדרטי של חלונות.

        private void btnSetColor_Click(object sender, EventArgs e)

        {

            using (ColorDialog dlg = new ColorDialog())

            {

                if (dlg.ShowDialog() == DialogResult.OK)

                    panelOverlay.DefaultDrawingAttributes.Color = dlg.Color;

            }

        }

פתחנו דיאלוג חדש ואם נבחר צבע - נשנה את הצבע של ה-DrawingAttributes לצבע שנבחר.

(כן, זה בהחלט מסוג הדברים שנהוג לראות את הילדה בת ה-3 של ליאור מציירת)

 

נראה דבר אחד אחרון - יצירת פונקציונליות Undo ללוח הציור שלנו.

בשביל כפתור ה-Undo נצטרך להבין מהי מחלקת ה-Stroke.
כל קו כזה שציירנו (כל משיכה של המכחול באנאלוגיה) מייצר מופע של מחלקה בשם Stroke שמתווספת לאוסף Stroke הקיים על ה-InkOverlay. האוסף הזה הוא קצת יותר מאוסף בנאלי של Stroke. אלא הוא מחלקה בשם Ink שיושבת על InkOverlay ומאפשרת לנו למחוק, להוסיף ולגשת לאוסף ה-Stroke בציור שלנו.

נצייר במשיכת מכחול ראשונה (בלי שנרים את העכבר) את המספר 1.
ובמשיכת מכחול שנייה נצייר את המספר 2.

כעת באוסף ה-InkOverlay.Ink.Strokes שלנו יש שני Strokes.

נכתוב קוד שמוציא מהאוסף את ה-Stroke האחרון שנוסף, מוחק אותו ומורה על ציור מחדש של הפנאל.

        private void btnUndo_Click(object sender, EventArgs e)

        {

            Stroke strokeToDelete = panelOverlay.Ink.Strokes[panelOverlay.Ink.Strokes.Count - 1];

            panelOverlay.Ink.DeleteStroke(strokeToDelete);

            panel1.Invalidate();

        }

ובאמת בלחיצה על ה-Undo נקבל שיש Stroke אחד פחות על המסך.

למעשה ה-Strokes נותנות לנו מה שמוכר בתוכנות ציור מודרניות כ-Layers.

 

ניתן להוריד את קוד המקור שעבדנו עליו כאן - http://www.JustinAngel.Net/Files/WinformsPainting.zip.

Question from .Net Tapuz forum: How to run a .Net application on specific runtime version?

שאלה:

אני מעונין להוסיף קטע קוד שבודק איזו גרסה יש ל Framework‏ במחשב שמריץ אחת מהאפליקציות שלי.

אני יודע שיש ב VS2005‏ אפשרות ל ClickOne‏ שמבצע את הפעולה ברגע שיוצרים קובץ התקנה, אבל זה לא רלוונטי בשבילי כי אני מוסר רק את קבצי האסמבלי הרלוונטים לאפליקציה.

"הפכתי את האינטרנט" כדי למצוא קוד כזה ואני פשוט לא מוצא פתרון..

אשמח לקבל דוגמא לקטע קוד ב C‏# שיוכל לזרוק Exception‏  ברגע שקוד שנוצר על VS2005‏ מנסה לרוץ על פלטפורמה שאיננה Framework 2‏ (למשל Framework 1.1)

 

תשובה:

 בקובץ הקונפיגיורציה של האפליקציה שלך יש אלמנט מעניין שלרוב לא מזכירים בשם <startup>.

באלמנט הזה ניתן לפרט אלמנט <supportedRuntime> עם גירסת הפריימוורק שבקונטקסט שלה תרוץ התוכנה שלנו.

הנה דוגמה של האלמנט:  

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

    <startup>

        <supportedRuntime version="v1.1.4322"/>

        <supportedRuntime version="v1.0.3705"/>

        <supportedRuntime version="v2.0.50727"/>

    </startup>

</configuration>

לדוגמה, אם נרצה באפליקציית WinForms שנכתבה על Visual Studio 2005 שהיא תרוץ רק בקונטקסט של דוט נט 2.0 נשנה את קובץ ה-App.Config כך שיכיל את האלמנט בצורה הבאה:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

    <startup>

        <supportedRuntime version="v2.0.50727"/>

    </startup>

</configuration>

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

<configuration>

    <startup>

        <supportedRuntime version="v4.0.0"/>

    </startup>

</configuration>

נקבל את הודעת השגיאה הבאה בהריצת האפליקציה:

---------------------------
WinTesting.exe - .NET Framework Initialization Error
---------------------------
To run this application, you first must install one of the following versions of the .NET Framework:

v4.0.0

Contact your application publisher for instructions about obtaining the appropriate version of the .NET Framework.

 

 

קישור: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=99637513