ארכיון פוסטים מהקטגוריה ".NET"
דעה: ASP.NET WebForms – חלק ב'
יום חמישי, 12 ביוני, 2008ASP.NET מרגילה מתכנתים לעבוד בצורה בזבזנית ומחפירה בכל הקשור ל-UI. כאחד שעוסק שנים רבות בפיתוח ל-Web בשלל טכנולוגיות, כל הזמן היה דגש כמה שפחות להטריד את השרת. לא רק ש-ASP.NET WebForms לא עושה את זה, היא אפילו מעודדת להטריד כמה שיותר. וזה פשוט רע.
כמה פעמים ראיתי קוד כמו
void btn_Click(object sender,EventArgs e) {
Response.Redirect("Page.aspx?Param=Value");
}
יש למישהו מושג איזה תהליך קורה כשלוחצים על הכפתור עד שמגיעים ללינק?
אז תיאור קצרצר:
- לחיצה על הכפתור מפעילה רוטינות JS לבדוק שהטופס ואלידי בהקשר של אותו Validation Group של הכפתור (אם יש), ובנוסף שמה את הכפתור כ-EventTarget
- אם הכל כשורה – הטופס נשלח לשרת יחד עם כל ערכי הפקדים הקיימים על העמוד וכמובן ה-ViewState
- מגיעה בקשת HTTP לשרת, כל הרוטינות הרגילות, הפניות, יצירת HttpContext, הרצת כל ה-HttpModules – טעינת Session, Cache וכו'
- העמוד המכיל את הכפתור רץ: נוצר מופע מהמחלקה של העמוד
- כל שרשרת האירועים רצה, אתחול culture, קונטרולים, init, load, render וכמובן שרשרת האירועים של קונטרולים מקוננים בעמוד שלנו
- אירוע ה-Click ששמנו לכפתור, בין אם ב-aspx בעזרת asp:Button OnClick=btn_Click או בין אם בקוד בעזרת btn.Click+=btn_Click, מחווט ונרשם לכפתור
- השרת עכשיו בודק מי ה EventTarget שלו ומגלה שזה הכפתור. עכשיו הוא יורה את אירוע ה-Click של הכפתור ומגיע לנחלה – מבצע Response.Redirect.
עכשיו שלבים 3-5 רצים שוב עבור העמוד אליו הפנינו.
נשמע הגיוני? לי ממש לא.
<a href="Page.aspx?Param=Value">click</a> <button onclick="location.href='Page.aspx?Param=Value';">click</button>
זה הרבה יותר טוב.
אז נכון, לא כולם עושים את זה. אבל לשם דוט נט מובילה אנשים שלא מכירים את התחום. ובמלוא מובן המילה – זה רע.
חושבים שזה לא משפיע? המשתמשים/לקוחות שלכם כבר יודעים לזהות אתר עם סיומת aspx כאתר שהולך להיות איטי לגלוש בו. כל האינטראקציה הלוך ושוב בין הקליינט לסרבר כל כך מיותרת ומכבידה על הגלישה עצמה. חוויית משתמש נוראית.
התדמית של ASP.NET כל כך יורדת בגלל היישום התמוה שמיקרוסופט עשו, גם בעיני משתמשים ובטח בעיני מתכנתים אחרים.
תחשבו על הסיטואציה הבאה: אתם מטיילים עם העכבר שלכם על המסך. בכל תזוזה של פיקסל – כל הפיקסלים על המסך מתרנדרים מחדש. הופכים להיות שחורים לעשירית שנייה ומקבלים צבע.
עכשיו תשוו את זה ל-ASP.NET WebForms
דעה: ASP.NET WebForms
יום שישי, 4 באפריל, 2008נכתב במקור כתגובה לשאלתה של רותם בפורום .net בתפוז. ואם השקעתי כל כך הרבה, אז יאללה, שיהיה גם חומר לבלוג
שורה תחתונה – טכנולוגית WebForms היא אחד הדברים הכי גרועים שמיקרוסופט אי פעם הוציאה. הטכנולוגיה הזו לעולם תיחשב כקיצור דרך של מיקרוסופט למתכנתי VB6 לעולם ה-Web. להבדיל, שימו לב, מ-ASP.NET שהיא פלטפורמה נהדרת אם לא הטובה מכולן.
מיקרוסופט ניסו לחשוב בצורה עסקית – איך אנחנו נותנים למתכנתי ה-VB שלנו גישה מהירה לעולם ה-Web המתפתח עד מאוד? כמובן – ניצור לו את הכלים שאותם הוא מכיר, באותה צורה ובאותה סביבה! Drag & Drop של אובייקטים, מחלקה שמייצגת את הטופס הנוכחי, לחיצה כפולה מובילה לאירוע לכתיבת קוד עבור אירוע ברירת המחדל. אחד לאחד. זה באמת מעורר הערצה.
מכיוון שלא באפשרות מיקרוסופט להטמיע דפדפן חדש שבעצם ירנדר אפליקציה ב-100% מהמחשבים בעולם, הם היו חייבים להישאר באותו רעיון של הדפדפן הרגיל ותקשורת HTTP – רינדור של HTML, טפסים, שרת staeless.
אז אחד אחד, הפתרונות שלהם:
- את כל העמוד שלנו נעטוף ב-form אחד בלבד, נוסיף לו מספר שדות שישמרו את המצב הנוכחי של הפקדים שלנו (להלן – ViewState). מטרת הטופס היא לגרום לאינטראקציה עם השרת בכל שינוי/לחיצת כפתור. בשרת אנחנו הרי יכולים לעשות מה-בא לנו (דברי מיקרוסופט) ולהחזיר חזרה לקליינט דברים אחרים מאלה שהיו על העמוד. בלי ליצור אותם בצד לקוח. רק לעבד בצד שרת ולהחזיר את כל המידע מחדש בצורה העדכנית, עם ViewState חדש שתואם את מצב הפקדים החדש שלנו.
- לא ממש חייבים לדעת HTML מאחר ויש Drag & Drop לפקדים שנראה זהה לעבודה מול WinForms. ואם בכל זאת רוצים – שמות הפקדים זהים לשל WinForms.
- כל קונטרול, הוא עטיפה בגודל הר למשהו פיצפון. כך הפך לו </"input type="text" name="a> ומקבילו ה-Serverי ["Request.Form["a למשהו מפלצתי, שבמבט ראשון נראה חמוד – </"asp:TextBox runat="server" ID="a> ומקבילו ה-Serverי a.Text (סוג של גרמלין) אך מאחוריו יושב מנוע רב מימדים. מי שלא חושב כך – שיפתח בבקשה את Reflector ויעבור על System.Web.UI (בהצלחה)
. מעבר לזה – כל כפתור שנלחץ, כל DropDown ששונה וכו' גורמים לשרשרת תהליכים מטורפת. לחצת כפתור? שרפת קלוריה. לא שלך, של השרת. *כל* הנתונים שעל העמוד נשלחים לשרת. כל ערכי פקדי ה-TextBox שרונדרו ל-input ושאר חבריו, וכל ה-ViewState. עמוד ארוך? לא נורא, 50K upload של נתונים זה.. 7 שניות בממוצע? ה bandwidth גדל כל הזמן, למי אכפת ?:)
עוד קצת בנוגע ל-ViewState: יש לך GridView בעמוד? כל הנתונים שבו מקומפרסים Base64 לתוך ה-ViewState ונשארים שם. מעתה, כל שינוי סדר, לחיצה על כפתור שבכלל יושב במקום אחר בעמוד וכו' ישלחו את כל הנתונים האלה חזרה לשרת, גם אם ה-GridView הזה לא צריך להיות שם אחרי הלחיצה (השרת לא יודע מזה). היוזר סה"כ רצה לבצע login, וכל נתוני העמוד נשלחו. 50K במקום 50 bytes (כולל headers). - עיצוב (CSS) מיושם ע"י הפרופרטיז המוכרים – BackColor, BorderStyle.
- ב-JS בכלל לא צריך לגעת כי הכל כבר מיושם מאחורי הקלעים. ולידאציות, שליחת הטופס, יישום TreeView וכו'
- קונטרולים של Data כמו SqlDataSource כדי שנעשה את חיי המתכנתים כ"כ קלים.
ואז הגיע הבאזוורד – AJAX
פה מיקרוסופט נכנסה ללחץ. "מה נעשה מה נעשה" שאלה. הרי כל הקטע ב-AJAX זה JS שעושה דברים מגניבים, וחוצמזה פונה לשרת בדרך מינימלית ויודע לשלוח ולקבל חזרה מהשרת רק מה שצריך.
אז בא הפקד UpdatePanel – סוג של Panel שמכיל בתוכו פקדים. רוצה שהוא יתרפרש לבד בלי שכל העמוד יתרפרש? אין בעיה, זה אכן קורה. ועובד. נהדר, לא?
חה. לא. הרעיון ה*גרוע* מאחורי UpdatePanel הוא כזה: קח את כל ערכי הפקדים, את ה-ViewState, שלח לשרת, לא דרך form כי זה ירפרש את העמוד אלא דרך xmlhttp. בשרת תעשה כאילו קיבלת את הנתונים מתוך טופס אמיתי, תפעיל מחדש את כל ה PostBack מכניזם, צור את העמוד מחדש, תפעיל את שרשרת האירועים שלו ובסופו של דבר, תרנדר אותו ותגזור רק את המקטע שבתוך ה-UpdatePanel שלנו, ושלח אותו חזרה לקליינט, כמובן עם ViewState חדש.
כך, עטיפה של כל האתר ב UpdatePanel שקולה בדיוק ל-PostBack.
רציתם דברים מגניבים ב-JS? יש פקדי אנימציה (איטית בהשוואה לכל ספריית אפקטים אחרת ברשת).
גם ה-Extenderים של הפקדים הרגילים מורכבים ממכניזם די רחב (מדי), ועל אף שאני אוהב את יוזמת ה-Toolkit, הסירבול בהרחבה של פקדים שנכתבו שם הוא פשוט אבסורד (באף אפליקציה שהמחלקה שלי או אני כתבנו לא יכולנו להשתמש בפקד out of the box).
אני חייב לציין – אין לי שום דבר נגד מתכנתי VB או WinForms ויוזמה ברוכה של מיקרוסופט לקרבם לעולם ה-Web, אך המחיר שמשלמים בכך שהמנגנון המקורי של asp.net הוא מנגנון כבד ובזבזני הוא גדול.
ואני לא מדבר רק על ביצועים. אלא על זמן. זמן תסכול, זמן מחקר ב-Reflector מה-לעזאזל-הם-התכוונו, זמן קליטה ש-UpdatePanel עושה בעצם Full PostBack וש Partial PostBack זה FRAUD (תוך כדי חקירה ב-fiddler).
אוקיי, אחרי החפירה הארוכה הזו, אז מה עושים?
- מיקרוסופט מצידם עובדים סוף סוף על תחליף ל-WebForms שנקרא MVC – כרגע בבטא. זהו קונספט שבעיקרון קיים ב-PHP ו-Ruby On Rails, וגם קיים בפרוייקט castle עבור ASP.NET בשם monorail. מהבלוג של סקוט גו זה נראה טוב (יחי ה inline scripting)
- אפשר להפסיק לכתוב עם form runat=server. לעבוד פשוט מול הנדלרים (ashx) או web services.
- לא כל GridView או Repeater חייבים להיות כאלה. הם יכולים להיות inline code חמוד. זו לא עבירה. זה העתיד.
- AJAX – אין יותר פשוט מזה בצורה הטבעית שלו. עבודה מול web services עם ScriptService כדי לחשוף גם לפניות AJAX שלא מבוססות UpdatePanel או סתם לתוך ashx. אפשר לשלוח פניייה עם כל פריימוורק JS בסיסי כמו MooTools, jQuery, Prototype (אין טעם לכתוב לבד).
- בנוגע לשכפול לוגיקות — כותבים/מורידים פעם אחת בודק טפסים גנרי (יש כאלה שעובדים לפי class של input כמו input class=required) ויש לנו ולידאציות. בשרת נמשיך לעשות ולידאציות בשכבת הלוגיקה.
אחרי צעד כזה, פתאום מרגישים חופשיים ולא כבולים לכלום. וזמן הפיתוח מתקצר. באמת.
יש מאות אפליקציות web עשירות עם AJAX ו-JS מגניב מבוססות ROR או PHP. מישהו יכול למנות לי 10 בולטות מבוססות ASP.NET AJAX שגם מתקפדת טוב וגם נראית טוב?
הודעה זו נכתבה בשל תסכול יתר של שנים רבות מטכנולוגית ה-WebForms המיותרת להפליא. בזמן שמבזבזים על חקירת הקרביים של ה-WebForms, או מתחבטים בשאלות של "למה ה-ViewState לא נטען לי בפקדים דינמיים היררכיים" או יותר מזה – "למה הכל עובד נורא לאט?!" אפשר להשלים שעות שינה של שנתיים
תוסף Outline לקובצי JavaScript ב-Visual Studio 2005
יום חמישי, 6 במרץ, 2008מיקרוסופט שכחו את מתכנתי הקליינט סייד הכבדים ב-visual studio ולא הוסיפו את האופציה של collapse ו-expand לפונקציות בקובצי javascript.
מצאתי add-in (אחרי חיפוש קדחתני) חביב ל-visual studio 2005
הורדה – SmartOutline – Javascript Outline for Visual Studio 2005
(לא בדקתי על visual studio 2008, אם זה עובד אשמח לשמוע בתגובות ולעדכן)
#C: טיפוסים גנריים (Generic Types) וממשקים (Interfaces)
יום חמישי, 27 בספטמבר, 2007נניח שיש לנו את המחלקה הבאה:
public class ItemCollection<T> : List<T> {
public ItemCollection<T> Load() {
// load process here...
return this;
}
public ItemCollection<T> Attach(T item) {
base.Add(item);
return this;
}
public ItemCollection<T> Clone() {
ItemCollection<T> cloned=new ItemCollection<T>();
// clone logic here...
return cloned;
}
}
והתוכנית הבאה:
public class Program {
public static void Main(string[] args) {
ItemCollection<int> intCollection=new ItemCollection<int>();
intCollection.Attach(1).Load();
}
}
ItemCollection היא מחלקה שיורשת מ- List<T> ומוסיפה לה כמה מתודות. שימו לב שגם היא מקבלת T כ-type parameter ומעבירה אותו ל-List עצמו, כלומר תינתן לנו האופציה ליצור מופע עם כל טיפוס כפרמטר, ובתוך המחלקה עצמה נמשיך להתייחס אליו כאילו הוא T.
דבר נוסף, המתודות מחזירות את האובייקט עצמו לשם מימוש fluent interface (בקצרה – החזרת האובייקט מהמתודות שלו כדי שנוכל "לשרשר" מתודות במקום לכתוב את שם האובייקט בכל הפעלה של מתודה).
בתוכנית, יצרנו מופע של ItemCollection מסוג int, הפעלנו עליו את המתודה Attach שבמחלקה עצמה מקבלת T אך למופע שלנו ה-T ייחשב כ-int, עם הפרמטר 1. המתודה Attach החזירה את האובייקט עצמו ועכשיו אנחנו יכולים להפעיל עליו עוד מתודות, כמו Load.
ועכשיו, נניח שיש לנו עוד מחלקת עזר שמקבלת ItemCollection כזה ומוסיפה לו ערך.
public class Helper {
public void AddToItemCollection(ItemCollection<int> collection,int item) {
collection.Attach(item);
}
}
ועכשיו נוכל להעביר לה כפרמטר את האוסף ומספר ולהוסיף לו אותו
Helper.AddToItemCollection(intCollection,2);
אחלה! רק חבל שזה מתאים רק ל ItemCollection<int> . אם יהיה לנו ItemCollection<string>, נאלץ לכתוב מתודה נוספת. אז אנחנו יכולים להפוך את המתודה הזו לגנרית אף היא:
public static void AddToItemCollection<T>(ItemCollection<T> collection,T item) {
collection.Attach(item);
}
ולהוסיף לכל סוג של ItemCollection
ItemCollection<string> stringCollection=new ItemCollection<string>(); Helper.AddToItemCollection<string>(stringCollection,"hello");
כיף. אבל, בכל פעם שנרצה להעביר את המחלקה כפרמטר נאלץ להשתמש במתודות גנריות. תסכול מס' 1 :-S (האייקון הממורמר של מסנג'ר פה).
אהה! נממש interface עם המתודות האלה, מה כבר יכולה להיות הבעיה…
public interface IItemCollection {
}
ופה ניתקע. כאן המוח שלנו יתחיל להיכנס ללולאה אינסופית. אני רוצה להגדיר את המתודה Load, מה לעזאזל ערך ההחזרה שלה? נו בוודאי, ItemCollection<… אבל איזה T? אולי נעשה את הממשק גם הוא גנרי? יאללה, רק שלא נרוויח מזה מאום, כי גם כשנרצה להעביר את הממשק כפרמטר הטיפוס שלו יצטרך להכיל את פרמטר הטיפוס או לחילופין להיות גנרי. תסכול מס' 2 :-S.
אין דבר כזה בלתי אפשרי, ואנחנו נמשיך עם הממשק שלנו:
public interface IItemCollection {
IItemCollection Load();
IItemCollection Attach(object item);
IItemCollection Clone();
}
אם המחלקה שלנו תממש את הממשק הזה המתודות שלה יוכלו להחזיר את הממשק עצמו, מתודות שמתייחסות לטיפוס T יועברו כ-object ויעברו פעולת casting קטנה.
public class ItemCollection<T> : List<T>,IItemCollection {
public IItemCollection Load() {
// load process here...
return this;
}
public IItemCollection Attach(object item) {
base.Add((T)item);
return this;
}
public IItemCollection Clone() {
ItemCollection<T> cloned=new ItemCollection<T>();
// clone logic here...
return cloned;
}
}
עכשיו זה באמת טוב. מימשנו את הממשק, T הופך ל-object והרבה קאסטים גועליים רצים לנו בתוך הלוגיקה. תסכול מס' 3 :-S.
Explicit implementation to the rescue
למה שלא נמשיך להתעקש? בסוף תמיד נגיע למחוז חפצנו. בדוט נט אפשר להגדיר explicit implementation על interface, מה שנותן לנו את האופציה אכן להגדיר את המימוש למתודה אבל רק כאשר המחלקה שלנו מזוהה כאותו ממשק. נקפוץ לקוד:
public class ItemCollection<T> : List<T>,IItemCollection {
public ItemCollection<T> Load() {
// load process here...
return this;
}
public ItemCollection<T> Attach(T item) {
base.Add(item);
return this;
}
public ItemCollection<T> Clone() {
ItemCollection<T> cloned=new ItemCollection<T>();
// clone logic here...
return cloned;
}
IItemCollection IItemCollection.Load() {
return Load();
}
IItemCollection IItemCollection.Attach(object item) {
return Attach((T)item);
}
IItemCollection IItemCollection.Clone() {
return Clone();
}
}
כעת, המחלקה שלנו כוללת שני מימושים למתודות. ממשקים מחייבים הגדרה אחת של מתודה עם חתימה ספציפית. המימוש של Load שתופס עבור ה-interface הינו המימוש המחזיר IItemCollection. שימו לב לנוכחות ה IItemCollection+נקודה לפני המילה Load – זה אומר שזה המימוש של Load שאותו ממשק דורש. למימוש השני אין קשר לממשק, והוא לא מתנגש עם הממשק. מעתה נוכל להתייחס ל- IItemCollection<> גם כממשק ולהעביר אותו למתודות ללא בעיה.
קוד מתוך התוכנית
IItemCollection doubleCollection=new ItemCollection<double>(); Helper.AddToItemCollection(doubleCollection,3.4); Logger.Dump(doubleCollection);
המתודה מתוך ה-Helper
public static void AddToItemCollection(IItemCollection collection,object item) {
collection.Attach(item);
}
דוגמה נוספת
public interface IFirst {
IFirst A();
}
public interface ISecond {
ISecond A();
}
public class Class : IFirst,ISecond {
IFirst IFirst.A() {
Logger.Write("IFirst.A");
return this;
}
ISecond ISecond.A() {
Logger.Write("ISecond.A");
return this;
}
}
public class Helper {
public static void PrintFirstA(IFirst first) {
first.A();
}
public static void PrintSecondA(ISecond second) {
second.A();
}
}
קוד מתוך התוכנית:
Class c=new Class(); Helper.PrintFirstA(c); Helper.PrintSecondA(c); ((IFirst)c).A();
ידפיס על המסך
IFirst.A ISecond.A IFirst.A
שימו לב שאין מימוש public ל-A אלא מימוש explicit של interface. כל interface מכיל את המתודה A ו-Class נותן מימוש אחר לכל ממשק, כך שכשנתייחס למחלקה כ-IFirst היא תפעיל את IFirst.A וכנ"ל לגבי ISecond.