DCSIMG
March 2008 - Posts - Justin myJustin = new Justin( Expriences.Current );

March 2008 - Posts

[Tapuz .Net] ASP.Net 2.0 Wizard Control SideBar Rendering

שאלה:

האם ניתן להזיז את רשימת הצעדים ב-Wizard כך שתופיע מעל הפקדים (כמו טאבים)?

 

תשובה:

שאלה מצויינת שבאמצעותה נוכל לחפור קצת לתוך מערכת ה-Rendering של ASP.Net 2.0.

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

image

 

ניצור דף ASP.Net חדש שעליו נדגים את הבעיה.

image

נוסיף לטופס שלנו פקד Wizard.

image

וקיבלנו את הפקד Wizard הברירת מחדל:

image

בשביל הדוגמה נעשה כמה שינויים קוסמטיים קטנים.

נגדיל את הפקד.

image

נעשה לו Auto-Format שיהיה יותר וויזאולי ברור מה אנחנו מנסים לעשות.

image

image

נוסיף עוד שני Steps ל-Wizard.

image

image

image

ולבסוף, נוסיף קצת טקסט לכל Wizard Step שלנו.

image

image

כנ"ל נעשה גם לצעד 2 וצעד 4.

 

זהו, הקמנו דוגמה קטנה ל-Wizard של ASP.Net 2.0.

נריץ את הדוגמה.

image

בלחיצה על Next נעבור לשלב הבא.

image

 

עכשיו, השאלה היא איך ניתן להעביר את ה-Sidebar לחלק העליון של פקד ה-Wizard.

אין אפשרות מובנית כזאת.

בואו נביט על ה-HTML שמייצר ה-Wizard שלנו.
אני אשתמש ב-Internet Explorer Developer Toolbar בשביל זה.
גולשי FireFox יכולים להשתמש ב-FireBug.
ואלו מכם שגולשים בדפדפנים אחרים יכולים להשתמש ב-Notepad לעיין ב-HTML.

נבחר ב-Select Element By Click.

image

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

image

image

בחרנו את ה-Wizard שלנו ונוכל לראות את ההיררכיית פקדים שלו ובתוכו.

image

אפשר לראות ש-Wizard1 מתרגם לתגית <table> ב-HTML.

בתוך ה-<table> הראשי יש שורה אחת של תגית <tr> ובתוכו יש שני תאי <td> ובתוך כל אחד מאלו עוד טבלה <table>.

למי מאתנו שמבין באיך מעצבים HTML ומזועזע מהחוסר בשימוש בתגיות <div>ים כאן, אני מסכים. אם כי, שמפתחים פריימוורק צריך לרוב לבחור באפשרות שיותר קל לתמוך ולקסטם.

 

צריך לדאוג שה-Wizard ירנדר בטבלה שלו שתי שורות עם תא אחד בכל אחד מהם, ולא שורה אחת עם שני תאים.

נפתח את Reflector על System.Web.UI.WebControls.Wizard.

image

 

הדבר הראשון שתופס לנו את העין צריך להיות הירושה מ-CompositeControl.
CompositeControl היא סוג ספציפי של Custom Control ב-ASP.Net.
הרעיון הוא ש-Custom Control הוא פקד "יחיד" ו-Composite Control הוא איחוד בין מספר פקדים.

מהיכרות עם Composite Controls אנחנו יודעים לפתוח ישר את מתודת ה-CreateChildControls שבתוכה מפרטים תמיד איזה פקדים מתווספים להיררכיה.

image

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

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

image

 

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

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

אני הבנתי מהקוד הזה דבר אחד ל-this.Controls מוסיפים טבלה ראשית כלשהי.
אז לפי דעתי, אם נירש (גם באנגלית: Inherit) את Wizard ונדרוס את המתודה הזאת נוכל לשנות את אוסף הפקדים הפנימי.

 

נתחיל בלפתוח פרוייקט חדש מסוג Web Control Library.

image

image

שימו לב שב-Visual Studio 2008 שם הפרוייקט השתנה מ-ASP.Net Web Control Library ל-ASP.Net Server Control.

 

נוסיף פקד myWizard שיורש מ-Wizard.

image

image

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

using System.Web.UI.WebControls;

 

namespace WebControlLibrary_VS2008

{

    public class myWizard : Wizard

    {

 

    }

}

רק נדאג להוסיף את ה-Reference בין הפרוייקט Web שלנו ל-Web Control Library ונתקדם עם הפקד.

image

image

 

הגענו עכשיו לכתיבת הפקד שלנו.

נדרוס את המתודה CreateControlHierachy וכרגע גם הוספתי אליו BreakPoint כדי שעוד מעט נתחקר את this.Controls.

image

 

עכשיו נשנה את הקוד ASP.Net הקיים שלנו שיעבוד עם myWizard ולא ה-Wizard הברירת מחדל.

בקובץ ה-web.config שלנו נוסיף הרשמה ל-Web Control Library שלנו.

        <pages>

            <controls>

                <add assembly="WebControlLibrary_VS2008" namespace="WebControlLibrary_VS2008" tagPrefix="my" />

            </controls>

        </pages>

ובמקום להשתמש ב-<asp:Wizard> נשתמש בתגית ה-<my:myWizard>.

<asp:Wizard ID="Wizard1" runat="server">
...
</asp:Wizard>

 
 

<my:myWizard ID="Wizard1" runat="server">
...
</my:myWizard>

 

עכשיו שנריץ את האפליקציה Visual Studio עצר בשורה שביקשנו שיעצור.

image

נפתח את חלון ה-Quick Watch באמצעות קיצור המקלדת Ctrl + Alt + Q ונבקש ממנו לבדוק את this.Controls.

image

נחפור קצת בהיררכית הפקדים.

image

image

אפשר לראות שיש לנו WizardControlCollection שמכיל WizardChildTable שמכיל RowControlCollection שמכיל TableRow וזה מכיל בתורו AccessiableTableCell שהוא ה-SideBar ו-TableCell נוסף שזה ה-Wizard עצמו.

 

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

        protected override void CreateControlHierarchy()

        {

            base.CreateControlHierarchy();

 

            Control wizardRows = this.Controls[0];

            TableCell sideBar = (TableCell) wizardRows.Controls[0].Controls[0];

            TableCell wizard = (TableCell) wizardRows.Controls[0].Controls[1];

        }

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

        protected override void CreateControlHierarchy()

        {

            base.CreateControlHierarchy();

 

            Control wizardRows = this.Controls[0];

            TableCell sideBar = (TableCell) wizardRows.Controls[0].Controls[0];

            TableCell wizard = (TableCell) wizardRows.Controls[0].Controls[1];

 

            sideBar.Parent.Controls.Remove(sideBar);

        }

ולבסוף, ניצור TableRow חדש שלתוכו נכניס את ה-TableRow ונשים את ה-TableRow כשורה הראשונה בטבלת השורות.

        protected override void CreateControlHierarchy()

        {

            base.CreateControlHierarchy();

 

            Control wizardRows = this.Controls[0];

            TableCell sideBar = (TableCell) wizardRows.Controls[0].Controls[0];

            TableCell wizard = (TableCell) wizardRows.Controls[0].Controls[1];

 

            sideBar.Parent.Controls.Remove(sideBar);

 

            TableRow sideBarRow = new TableRow();

            sideBar.Height = new Unit(10);

            sideBarRow.Controls.Add(sideBar);

            wizardRows.Controls.AddAt(0, sideBarRow);

        }

 

אם נריץ את הדוגמה כעת, זה יראה כך:

image

ואם נלחץ על Next נקבל:

image

 

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

זאת גם הזדמנות טובה להדגים Encapsulate Field Refactoring ו-Extract Method Refoactoring.

 

נוסיף שדה בוליאני בשם showToBar_ ונאתחל אותו כברירת מחדל ל-false.

    public class myWizard : Wizard

    {

        protected override void CreateControlHierarchy()

        {

            ...

        }

 

        private bool _showTopBar = false

    }

בתוך Visual Studio 2008\2005 נלחץ Ctrl + R, Ctrl + E או כפתור ימני על שם השדה ונבחר Refactor --> Encapsulate Field.

image

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

    public class myWizard : Wizard

    {

        protected override void CreateControlHierarchy()

        {

            ...

        }

 

        private bool _showTopBar = false;

 

        public bool ShowTopBar

        {

            get { return _showTopBar; }

            set { _showTopBar = value; }

        } 

    }

נוסיף גם קצת מאפיינים ל-Design Time כדי שלמאפיין יהיה ערך ברירת מחדל ויופיע במקום הנכון ב-Properties של הפקד.

        private bool _showTopBar = false;

        [Category("Behavior"), DefaultValue(false)]

        public bool ShowTopBar

        {

            get { return _showTopBar; }

            set { _showTopBar = value; }

        } 

עכשיו נוציא את כל הקוד שלנו למתודה יעודית. נסמן את כל הקוד בתוך CreateControlHierachy.

image

נלחץ Ctrl + R, Ctrl + M או נלחץ כפתור ימני, אז Refactor ובסוף Extract Method.
נקבע שם חדש למתודה שתכיל את כל הקוד שסימנו.

image

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

    public class myWizard : Wizard

    {

        protected override void CreateControlHierarchy()

        {

            base.CreateControlHierarchy();

 

            moveSideBarToTopBar();

        }

 

        private void moveSideBarToTopBar()

        {

            Control wizardRows = this.Controls[0];

            TableCell sideBar = (TableCell)wizardRows.Controls[0].Controls[0];

            TableCell wizard = (TableCell)wizardRows.Controls[0].Controls[1];

 

            sideBar.Parent.Controls.Remove(sideBar);

 

            TableRow sideBarRow = new TableRow();

            sideBar.Height = new Unit(10);

            sideBarRow.Controls.Add(sideBar);

            wizardRows.Controls.AddAt(0, sideBarRow);

        }

 

        private bool _showTopBar = false;

        [Category("Behavior"), DefaultValue(false)]

        public bool ShowTopBar

        {

            get { return _showTopBar; }

            set { _showTopBar = value; }

        } 

    }

נוסיף תנאי if על המאפיין הבוליאני שלנו לפני שמבצעים את המתודה.

using System.ComponentModel;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace WebControlLibrary_VS2008

{

    public class myWizard : Wizard

    {

        protected override void CreateControlHierarchy()

        {

            base.CreateControlHierarchy();

 

            if (ShowTopBar)

                moveSideBarToTopBar();

        }

 

        private void moveSideBarToTopBar()

        {

            Control wizardRows = this.Controls[0];

            TableCell sideBar = (TableCell)wizardRows.Controls[0].Controls[0];

            TableCell wizard = (TableCell)wizardRows.Controls[0].Controls[1];

 

            sideBar.Parent.Controls.Remove(sideBar);

 

            TableRow sideBarRow = new TableRow();

            sideBar.Height = new Unit(10);

            sideBarRow.Controls.Add(sideBar);

            wizardRows.Controls.AddAt(0, sideBarRow);

        }

 

        private bool _showTopBar = false;

        [Category("Behavior"), DefaultValue(false)]

        public bool ShowTopBar

        {

            get { return _showTopBar; }

            set { _showTopBar = value; }

        } 

    }

}

נחזור לפרוייקט ASP.Net שלנו ושנה את ShowTopBar ל-true.

image

 

אכן עכשיו שקבענו את ShowTopBar=True נקבל את ה-SIdeBar מעל ה-Wizard.

image

 

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

image

 

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

 

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

[Tapuz .Net] Winfrosm DateTimerPicker ValueChanged and Windows Messages

שאלה:

אני רוצה ליצור מתודה שתפעל כל פעם שנבחר תאריך ב-DateTimePicker.
הכוונה היא לא כל פעם שנבחר תאריך שונה (ValueChanged) אלא יכול להיבחר לדוגמה גם אותו תאריך שהיה קודם.
יש אפשרות לעשות כך? אולי בשיטה עוקפת?

 

תשובה:

זאת שאלה מצויינת שבאמת תאפשר לנו להיכנס לעומק הקורה עם Windows Forms.

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

 image

 

נתחיל בלהדגים את הבעיה.
נפתח טופס Winform חדש.

image

נגרור לטופס פקד DateTimerPicker.

image

 image

כמו כן, נוסיף לטופס TextBox שאליו נדפיס טקסט.

image

נירשם לאירוע ה-ValueChanged של ה-DateTimePicker שלנו ע"י לחיצה כפולה על ValueChanged.

image

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

       private void dateTimePicker1_ValueChanged(object sender, EventArgs e)

        {

            tbxConsole.Text += dateTimePicker1.Value.ToShortDateString() + Environment.NewLine;

        }

 

בואו נריץ את האפליקציה.

image

נבחר את תאריך ה-25.2.2008.

image

image

נכתוב בתוך ה-DateTimerPicker שלנו את התאריך 27.2.008.

image

עכשיו ננסה לבחור שוב את ה-27.2.2008.

image

image

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

שזה אומר - ValueChanged יקפוץ אך ורק כאשר Value באמת משתנה.
השאלה שאנחנו מנסים לענות - איך לתפוס כל קביעה של תאריך ולא רק שינוי תאריך.

 

נפתח את Reflector על DateTimePicker במתודת ה-OnValueChanged שמריצה את ValueChanged.
(Reflector --> System.Windows.Forms --> System.WindowsForms.dll --> System.Windows.Forms --> DateTimePicker)

נבחר שאנחנו רוצים לראות מי קורא למתודת ה-OnValueChanged.

image

 image

 

אפשר לראות שיש 3 פונקציות בתוך הפריימוורק שמרימות את האירוע.
הראשונה היא ResetValue והגיוני שהיא תרים אירוע שינוי ל-Value.
השנייה היא set_value שמתייחסת ל-Setter של המאפיין (גם באנגלית: Property) בשם Value. גם פה הגיוני שהאירוע ירוץ.
והשלישי הוא WmDateTimerChange שמקבל איזה פרמטר מוזר מסוג &Message.

 

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

image

נבדוק מה הקוד שיש בתוך המתודה הזאת בפריימוורק.

image

image

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

עכשיו שמצאנו את האשם הראשי - נמצא את השותפים לפשע.

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

נראה מי קורה למתודה הזאת (דרך Analyze כפי שעשינו קודם לכן).

image

 

אפשר לראות כאן שרשרת של פונקציות של WndProc שקורה ל-WmReflectCommand שקורה ל-WmDateTimeChanged.

אני עדיין בכלל לא יודע מה זה WndProc ומה זה &Message ובכלל מה זה Wm.

נראה מה התוכן של שתי המתודות האלו.

image

image

אם נעקוב אחרי התהליך נראה ש-WndProc מקבל איזה פרמטר מוזר ואם הוא מקבל Message.Msg == 0x204e הוא יקרה ל-WmReflectCommand.
ב-WmReflectCommand אם נקבל IParamCode.code == -759 אז נקרא ל-WmDateTimeChange שבתורה אנחנו יודעים שמרים את אירוע ה-ValueChanged.

 

זאת השרשרת שגילינו.

image

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

 

זהו, זהינו לחלוטין את האשמים בבעיה שיש לנו.

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

 

נפתח פרוייקט חדש מסוג Windows Control Library ב-Visual Studio 2008.

image

image

 

ניצור מחלקה (גם באנגלית: Class) חדשה בשם myDateTimerPicker בתוך הפרוייקט Windows Forms Control Library החדש שלנו.

image

image

 

נדאג לשנות את הקובץ ברירת מחדל כך שהמחלקה תירש (גם באנגלית: Inherit) את DateTimerPicker.

using System.Windows.Forms;

 

namespace WinControls_VS2008Testing

{

    public class myDateTimerPicker : DateTimePicker

    {

    }

}

 

בתור התחלה לפתרון, נרצה לחשוף אירוע חדש בשם ValueChanging שירוץ לפני ValueChanged.

    public class myDateTimerPicker : DateTimePicker

    {

        public event EventHandler ValueChanging;

 

        public void OnValueChanging(EventArgs e)

        {

            if (ValueChanging != null)

            {

                ValueChanging(this, e);

            }

        }

    }

הקוד יחסית בסיסי, הוספנו אירוע בשם ValueChanged ומתודת OnValueChanging שתעלה את האירוע.

 

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

    public class myDateTimerPicker : DateTimePicker

    {

        protected override void WndProc(ref Message m)

        {

            base.WndProc(ref m);

        }

 

        public event EventHandler ValueChanging;

 

        public void OnValueChanging(EventArgs e)

        {

            if (ValueChanging != null)

            {

                ValueChanging(this, e);

            }

        }

    }

אז דרסנו את WndProc.
כרגע הוא רץ בדיוק אותו דבר כמו קודם ועוד אין שינוי.

נממש את שני התנאים מקודם לכן (שני ה-Ifים בתוך WndProc ו-WmReflectCommand).

using System;

using System.Runtime.InteropServices;

using System.Windows.Forms;

 

namespace WinControls_VS2008Testing

{

    public class myDateTimerPicker : DateTimePicker

    {

        protected override void WndProc(ref Message m)

        {

            if ((m.Msg == 0x204e)

                && (m.HWnd == base.Handle)

                && (((NMHDR)m.GetLParam(typeof(NMHDR))).code == -759)

                )

            {

            }

 

            base.WndProc(ref m);

        }

 

        [StructLayout(LayoutKind.Sequential)]

        private struct NMHDR

        {

            public IntPtr hwndFrom;

            public IntPtr idFrom;

            public int code;

        }

 

 

        public event EventHandler ValueChanging;

 

        public void OnValueChanging(EventArgs e)

        {

            if (ValueChanging != null)

            {

                ValueChanging(this, e);

            }

        }

    }

}

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

וכשלב אחרון בפתרון, נעלה בתנאים אלו את אירוע ה-OnValueChanging שלנו.

   public class myDateTimerPicker : DateTimePicker

    {

        protected override void WndProc(ref Message m)

        {

            if ((m.Msg == 0x204e)

                && (m.HWnd == base.Handle)

                && (((NMHDR)m.GetLParam(typeof(NMHDR))).code == -759)

                )

            {

                OnValueChanging(new EventArgs());

            }

 

            base.WndProc(ref m);

        }

 

        [StructLayout(LayoutKind.Sequential)]

        private struct NMHDR

        {

            public IntPtr hwndFrom;

            public IntPtr idFrom;

            public int code;

        }

 

 

        public event EventHandler ValueChanging;

 

        public void OnValueChanging(EventArgs e)

        {

            if (ValueChanging != null)

            {

                ValueChanging(this, e);

            }

        }

    }

 

בואו נראה שהפתרון שלנו באמת פועל.

צריך לדאוג להוסיף Reference בין שני הפרוייקטים.

image

image

 

אחרי שנקמפל את כל ה-Solution נוכל לראות ב-Toolbox של הטופס שלנו את myDateTimePicker מתחת ל-Windows Forms Control Library שלנו.

image

 

נשים myDateTimePicker על הטופס.
(שלנו זה התחתון)

image

נירשם לאירוע ה-ValueChanging ושם נדאג להדפיס ל-TextBox.

image

        private void myDateTimerPicker1_ValueChanging(object sender, EventArgs e)

        {

            tbxConsole.Text += myDateTimerPicker1.Value.ToShortDateString() + Environment.NewLine;

        }

 

נריץ את האפליקציה.

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

image

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

image

 

עכשיו בואו נבין מה בכלל הולך כאן.

מה זה ה-Message הזה שעובר הלוך ושוב.

Windows Forms בכלל הוא ברובו שכבה של קוד דוט-נטי מעל אלמנטים חלונאיים סטנדרטיים.

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

image

למעשה Winforms יוצר כדי לטווח בין תכנות Object-Oriented שהוא הרבה יותר אינטואטיבי לבין תכנות Win32 שהוא הרבה יותר פרוצדוראלי.

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

 

נסכם מה שעשינו.

image

 

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

 

 

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