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

January 2008 - Posts

[Tapuz .Net] Getting WPF's FlowDocument and FlowDoucmentReader MouseOver text

Question:

I've got a WPF FlowDocument in a FlowDocumentReader.
On mouse left button click I need to get the "word" that the user clicked on.
How can I get that "word"?

 

Answer:

First, let's understand the problem.

To those of you not familiar with the WPF FlowDocument check out this XAML file: www.sneath.org/tim/chocolate.xaml
If you've got .Net 3.0 installed on your machine, it will automatically render and run it inside your browser.
image

We can change the Font-size with the top-right scroller.

image 
Fundamentally the FlowDocument with it's various presenters is meant to display real-world readable text.


So let's see what the problem is.

Let's add a new WPF XAML window.

image

image

We get the following XAML in our empty window.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

 

    </Grid>

</Window>

Let's add a new FlowDoucmentReader.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

 

        </FlowDocumentReader>

    </Grid>

</Window>

And we will add a random FlowDocument in our FlowDocumentReader. Let's copy the Example FlowDocument from MSDN2.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

            <FlowDocument>

                <Paragraph FontSize="18">Flow Format Example</Paragraph>

 

                <Paragraph>

                    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy

      nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi

      enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis

      nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure.

                </Paragraph>

                <Paragraph>

                    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh

      euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim

      ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl

      ut aliquip ex ea commodo consequat. Duis autem vel eum iriure.

                </Paragraph>

 

                <Paragraph FontSize="18">More flow elements</Paragraph>

                <Paragraph FontSize="15">Inline, font type and weight, and a List</Paragraph>

 

                <List>

                    <ListItem>

                        <Paragraph>ListItem 1</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 2</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 3</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 4</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 5</Paragraph>

                    </ListItem>

                </List>

 

                <Paragraph>

                    <Bold>Bolded</Bold>

                </Paragraph>

                <Paragraph>

                    <Underline>Underlined</Underline>

                </Paragraph>

                <Paragraph>

                    <Bold>

                        <Underline>Bolded and Underlined</Underline>

                    </Bold>

                </Paragraph>

                <Paragraph>

                    <Italic>Italic</Italic>

                </Paragraph>

 

                <Paragraph>

                    <Span>The Span element, no inherent rendering</Span>

                </Paragraph>

                <Paragraph>

                    <Run>The Run element, no inherent rendering</Run>

                </Paragraph>

 

                <Paragraph FontSize="15">Subscript, Superscript</Paragraph>

 

                <Paragraph>

                    <Run Typography.Variants="Superscript">This text is Superscripted.</Run> This text isn't.

                </Paragraph>

                <Paragraph>

                    <Run Typography.Variants="Subscript">This text is Subscripted.</Run> This text isn't.

                </Paragraph>

                <Paragraph>

                    If a font does not support a particular form (such as Superscript) a default font form will be displayed.

                </Paragraph>

 

                <Paragraph FontSize="15">Blocks, breaks, paragraph</Paragraph>

 

                <Section>

                    <Paragraph>A block section of text</Paragraph>

                </Section>

                <Section>

                    <Paragraph>Another block section of text</Paragraph>

                </Section>

 

                <Paragraph>

                    <LineBreak/>

                </Paragraph>

                <Section>

                    <Paragraph>... and another section, preceded by a LineBreak</Paragraph>

                </Section>

 

                <Section BreakPageBefore="True"/>

                <Section>

                    <Paragraph>... and another section, preceded by a PageBreak</Paragraph>

                </Section>

 

                <Paragraph>Finally, a paragraph. Note the break between this paragraph ...</Paragraph>

                <Paragraph TextIndent="25">... and this paragraph, and also the left indention.</Paragraph>

 

                <Paragraph>

                    <LineBreak/>

                </Paragraph>

 

            </FlowDocument>

        </FlowDocumentReader>

    </Grid>

</Window>

In our Visual Studio 2008 Designer we'll see that our window looks like the following image.

image

And we can run it and see pretty much the exact same thing.

image

We can work with it like with any other FlowDocumentReader.

Here's me changing pages.

image

Resizing the Font-size.

image 

And so on.

 

The question refers to getting the text on the FlowDocumentReader in the current mouse position.

If we were working with a TextBlock or a RichTextBox we could have used the GetPositionFromPoint method.

image

But we don't have a FlowDocumentReader.GetPositionFromPoint method.

So I wrote up one quickly.

    public static class RunExtensions

    {

        public static TextPointer GetPositionFromPoint(/* this */ Run _this, Point searchForPoint)

        {

            TextPointer ptrCurCharcter = _this.ContentStart.GetNextInsertionPosition(LogicalDirection.Forward);

            TextPointer ptrNextCharcter = ptrCurCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

            while (ptrNextCharcter != null)

            {

                Rect charcterInsertionPointRectangle = ptrCurCharcter.GetCharacterRect(LogicalDirection.Forward);

                Rect nextCharcterInsertionPointRectangle = ptrNextCharcter.GetCharacterRect(LogicalDirection.Backward);

 

                if (searchForPoint.X >= charcterInsertionPointRectangle.X

                    && searchForPoint.X <= nextCharcterInsertionPointRectangle.X

                    && searchForPoint.Y >= charcterInsertionPointRectangle.Top

                    && searchForPoint.Y <= charcterInsertionPointRectangle.Bottom)

                {

                    return ptrCurCharcter;

                }

 

                ptrCurCharcter = ptrCurCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

                ptrNextCharcter = ptrNextCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

            }

 

            return null;

        }

    }

This method gets a Point and checks if there's a TextPointer on the "System.Windows.Documents.Run" that is in the bounds of that point.

Now we've got to hook this up to the FlowDocumentReader.
I'm using a few Logical Tree Helper methods I've got on the side.

    public static class LogicalTreeHelperHelper

    {

            public static IEnumerable GetChildren(DependencyObject obj, Boolean AllChildrenInHierachy)

            {

                if (!AllChildrenInHierachy)

                    return LogicalTreeHelper.GetChildren(obj);

 

                else

                {

                    List<object> ReturnValues = new List<object>();

 

                    RecursionReturnAllChildren(obj, ReturnValues);

 

                    return ReturnValues;

                }

            }

 

        private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)

        {

            foreach (object curChild in LogicalTreeHelper.GetChildren(obj))

            {

                returnValues.Add(curChild);

                if (curChild is DependencyObject)

                    RecursionReturnAllChildren((DependencyObject)curChild, returnValues);

            }

        }

 

        public static IEnumerable<ReturnType> GetChildren<ReturnType>(DependencyObject obj, Boolean AllChildrenInHierachy)

        {

            foreach (object child in GetChildren(obj, AllChildrenInHierachy))

                if (child is  ReturnType)

                    yield return (ReturnType)child;

        }

    }

Real quick - this GetChildren method gets a bool saying if it's to return all children in the class hierarchy or just the first level one.
The overloaded Generic GetChildren gets a generic type "ReturnType" and filters all the children based on that type.

 

One way to get the TextPointer is by registering to the FlowDocumentReader.MouseDown event.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

            <FlowDocument MouseDown="FlowDocument_MouseDown">

                ...

            </FlowDocument>

        </FlowDocumentReader>

    </Grid>

</Window>

And on the MouseDown event call a method that iterates over all children of type "Run" and uses GetPositionFromPoint with the current mouse position. 

        private void FlowDocument_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

            TextPointer ptr = FlowDocumentReaderExtensions.GetPositionFromPoint(flowRdr1, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

            }

        }

All the FlowDocumentExtensions.GetPositionFromPoint does is iterate over all Run inlines on the FlowDocument and checks their RunExtensions.GetPositionFromPoint.

    public static class FlowDocumentReaderExtensions

    {

        public static TextPointer GetPositionFromPoint(/*this*/ FlowDocumentReader _this, Point searchForPoint)

        {

            foreach (Run curRun in LogicalTreeHelperHelper.GetChildren<Run>(_this, true))

            {

                TextPointer ptr = RunExtensions.GetPositionFromPoint(curRun, searchForPoint);

                if (ptr != null)

                    return ptr;

            }

 

            return null;

        }

    }

 

Let's run this and Click on the "U" in "Lorm Ipsum".

image

Using the debugger we can see the values we get from FlowDocument.GetPositionFromPoint.

image

image

Using these two strings, we can get the current "word" the mouse is over.

        private void FlowDocument_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

            TextPointer ptr = FlowDocumentReaderExtensions.GetPositionFromPoint(flowRdr1, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

 

                string[] textsAfterCursor = textAfterCursor.Split('.', ' ');

                string[] textsBeforeCursor = textBeforeCursor.Split('.', ' ');

 

                string currentWord = textsBeforeCursor[textsBeforeCursor.Length - 1] + textsAfterCursor[0];

 

            }


image 

 

Tamir Khason suggested a good way to improve performance on this one.
We can
attach a MouseDown event to each Run before hand and thus save the recursion over the logical WPF control tree.

        private void Window_Loaded(object sender, RoutedEventArgs e)

        {

            foreach (Run curRun in LogicalTreeHelperHelper.GetChildren<Run>(flowRdr1.Document, true))

                curRun.MouseDown += new MouseButtonEventHandler(curRun_MouseDown);

        }

 

        void curRun_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

           Run senderRun = (Run) sender;

            TextPointer ptr = RunExtensions.GetPositionFromPoint(senderRun, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

 

                string[] textsAfterCursor = textAfterCursor.Split('.', ' ');

                string[] textsBeforeCursor = textBeforeCursor.Split('.', ' ');

 

                string currentWord = textsBeforeCursor[textsBeforeCursor.Length - 1] + textsAfterCursor[0];

 

            }

        }

 

You can download all the sample code at - www.JustinAngel.Net/files/Example-WPF-FlowDocument-GetPositionFromPoint.zip.

 

Link: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=112005511

חוויות של יועץ דוט נט עצמאי - פרודוקס הטכנולוגיות חדשות

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

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

 

OK, והפעם - על פרדוקס הלקוחות החדשים והטכנולוגיות החדשות.

image

 

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

שהתחלתי עם דוט נט בשנת 2002 זה היה עוד טכנולוגיה חמודה שלמדתי בנוסף ל-ASP.
אז מה למדנו בשנת 2002?

* דוט נט 1.0 - שכלל את Winforms, ADO.Net, Webservice ו-ASP.Net בעיקר ועוד כמה חלקים מעניינים בפריימוורק (כ-5,756 מחלקות ציבוריות סה"כ).
* למדנו להכיר את Visual Studio 2003 שהיה שדרוג משמעותי מבחינת יציבות ל-VS2002 שאיתו עבדנו ב-COM.
* וכמובן שפת #C ו-VB.Net

image

(מתוך Moving Java Applications to .NET)

image

כמובן שבד-בבד למדנו סיקוול 2000 לרמה הרבה יותר גבוהה ממה שהכרנו עד כה.

image

 

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

במבט לאחור על השנים 2002, 2003 ו-2004 אני מבין שמיקרוסופט למעשה עשתה מה שפומה עושה לטרף שלו.
באותם שנים מיקרוסופט הבינה שיש לאיפה לרוץ קדימה, ארבה למפתחים בדשא הגבוה ואגרה תחמושת לעתיד.
למשל, מנהל הפרוייקט של WPF שהיא חלק מדוט נט 3.0 אומר מפורשות שהפיתוח שלה הפלטפורמה החל בשנת 2002.

מיקרוסופט זה כמו פומה

 

ואז הגיע שנת 2004, והדיבורים התחילו.
בחדרי חדרים כבר התחילו לדבר על סיקוול 2005 ודוט נט 2.0.
בסוף השנה כבר אמרו את בפה מלא - "בואי בואי חמודה, תורידי אבקה לבנה גירסת בטא של אבא'לה, זה טוב, זה טוב".

image

מיקרוסופט זה כמו סוחרי סמים 

(צילום אילוסטרציה.
המצולם באילוסטרציה רץ אחר כך והתקין Visual Studio 2020 כדי לקבל עוד היט של בטא ישר לזרוע)

למי שלא זוכר, שם הקוד של ל-VS2005 היה Whidbey ושם הקוד של סיקוול 2005 היה Yukon.

אני רוצה לחשוב שבשנת 2004-2005 עשיתי בחוכמה - לא התקנתי את VS2005, דוט נט 2.0 וסיקוול 2005 עד שהם יצאו.
אני זוכר שישבתי ב-Launch של דוט נט 2.0 פה בארץ בקהל בשקט וצחקתי עם אנשים על מה ההתלהבות הגדולה.

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

 

יצא דוט נט 2.0  ו-VS2005.

image

 

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

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

VS2005 היה יציב ופרודקטיבי פי אלף מ-VS2003.

ביחד עם דוט נט 2.0 ו-VS2005 גם יצא סיקוול 2005.

image

 

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

אבל הכי חשוב - הרגו את Enterprise Manager ונתנו לנו את Management Studio.

image

image

למי שלא מכיר וזוכר - Enterprise Manager בוסס על טכנולוגיה של חלונות 3.1. EM כל-כך ידידותי למשתמש ששמחתי כאשר התוכנה לא הוציא עוזי ופשוט ירתה לי ברגל.
Management Studio לעומת זאת, היה פאר היצירה. כלי שימושי, יעיל ונוח.

ביחד עם דוט נט 2.0 גם הייתה ההשקה הרשמית Team Foundation Server.
מפלצת של דבר שעושה כל מה שצריך בתחום ה-Configuration Management. אם כי TFS לא באמת מתאימה לאף אחד Out of the box.

image

 

 

עד אז היו לנו רק שתי פרוייקטים מיקרוסופטיים משמעותיים שלא היו חלק מהחבילה הרשמית - Enterprise Library שהייתה לא שמישה בעליל בגירסה 1.0  ופרוייקט ה-WSE 2.0 שנתן יכולות נוספות ל-Webserivceים.

ואז קרה משהו שזעזע אותנו עד עמקי נשתמנו - Atlas. *פם פם פם*

image

שלימים הפך להיות Microsoft AJAX.
אישית, הייתי בשוק. מיקרוסופט מוציאה פרוייקט צדדי קטן בבטא שבאמת היה שימושי.
אני זוכר שהייתי המום לגמרי שראיתי את ההדגמות של Atlas דצמבר 2005 CTP.

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

 

ויום אחרי ההשקה של דוט נט 2.0 כבר דיברנו על WinFx.
ובתוכם על Avalon (כיום נקרא WPF) ו-Indigo (כיום נקרא WCF).

image

מה? יום אחרי ההשקה של דוט נט 2.0 מיקרוסופט כבר דוחפת טכנולוגיה חדשה? ב-CTP?
ועוד גירסה של Visual Studio? משהו שנקרה Visual Studio Orcas?

image


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

מיקרסופט רדמונד

פחות משנה אחרי ההשקה של דוט נט 2.0 , כבר בשנת 2006-2007 יצא דוט נט 3.0.
ביחד איתו כבר היו דיבורים על דוט נט 3.5 (LINQ) והייתה ההשקה של Microsoft AJAX.

image

 

image

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

היו אלו שפשוט החליטו להפסיק לנסות להתעדכן.
עובד מיקרוסופט פעם אמר לי "אני לא קורא בלוגים. זה פשוט בלתי אפשרי. ברגע שאתה נכנס לשם יש כל-כך הרבה חומר ואתה טובע".
אותו עובד שהרצה על דוט נט 2.0, עבר מלדחוף את דוט נט 3.0 ללדחוף את דוט נט 3.5 בהרצאות LINQ. היום הוא כבר מרצה על SIlverlight.

רק עכשיו בסוף 2007 הייתה ההשקה של Visual Studio 2008 ודוט נט 3.5.

image

 

כרגע אנחנו עוד מתמודדים טכנולוגיות בבטא כמו Silverlight, סיקוול 2008 ו-Microsoft ASP.Net Futures.

image

 

למה אני מדבר בכלל המבול הטכנולוגי הזה?

הפרדוקס. טכנולוגיות חדשות ולקוחות חדשים.

image

מה קדם למה?
היועץ הטכנולוגי או הטכנולוגיה החדשה?

 

80% מכל העבודות יעוץ שאני עושה היום התחילו מהתעניינות של לקוחות בטכנולוגיות של דוט נט 3.0 וצפונה.
קרי, Microsoft AJAX, דוט נט 3.0, דוט נט 3.5 ו-Silverlight.

רק 20% מכל העבודות יעוץ שלי לא התחילו מהטכנולוגיות האלו.

image

 

איך מזהים חברה טובה וחכמה?

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

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

או

"ג'סטין, יש לנו בעיה, אתה יכול לבוא ולעזור לנו לפתור אותה?"

 

עם חברות כאלו אני נהנה לעבוד.
ההמלצות שלי זה לא רק "תאמצו Clickonce של מיקרוסופט, תשתמשו ב-WCF של דוט נט 3.0, כאן תשתמשו ב-Microsoft AJAX".
זה גם:

"פה אפשר לעבוד עם פרוייקט קוד פתוח ActiveRecord של Castle Project"
"כאן אפשר להשתמש ב-Templates של Visual Studio"
"תתקינו Resharper או כל תוסף צד-שלישי ותראו איך העבודה מואצת"
"כאן אפשר לעבוד Object Oriented, בואו נשב ביחד ונעשה את זה"
"במקום הזה אפשר וצריך לשים Unit testing"
"בואו נתכנן את לו"ז הפרוייקט באמצעות User Stories בצורה Agileית"

 

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

image

לרוב תוכניתנים לומדים את הטכנולוגיה (המשבצת השמאלית העליונה) ומתחילים לעבוד.
שאני מגיע זה בשלב שהם גם מנסים ללמוד טכנולוגיות חדשות (המשבצת הימנית התחתונה).
נכון, אני מכיר מצויין ויש לי ניסיון עבודה ב-Microsoft AJAX ואת Silverlight ו-Net 3.5. ו-Net 3.0.

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

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

 

MVP אורן עייני (מצוטט ברשות) שהוא יועץ שכיר מוכר בתחום אמר לי השבוע "הגעתי לחברה הזאת כדי ללמד אותם את טכנולוגית NHibernate, אבל בסוף עשינו ביחד את כל הארכיטקטורה שלהם".

ככה זה אמור להיות!

image

אגב, ממש בימים אלו אורן הופך מיועץ שכיר בחברת !We ליועץ עצמאי. מומלץ בחום למעוניינים.

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

 

אז הנה הפרדוקס:
אם 80% מהחברות מתחילות איתי עבודת יעוץ בשביל טכנולוגיות חדשות,
איך זה שבסוף 80% מהעבודה זה לשפר פרודקטיביות, ללוות בפיתוח Object-Oriented ובאמת להיות עין בוחנת מקצועית?

 

Question from Tapuz .Net forum: Nested DataBound Controls

שאלה:

יש לנו GridView שנטען מ-DataTable כלשהו ובתוכו יש TemplateField עם DropDownList.

הרשימה של ה- DropDownList   אמורה להתמלא מ Datatable אחר.

איך אני טוען את ה DropDownList  לרשימה נפרדת משלו ?

 

תשובה:

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

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

image

לדף נוסיף GridView.

image

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

נתחיל בליצור מחלקה שתייצג את הבקבוקי וויסקי שלנו. ניצור קובץ חדש בשם WhiskeyBottle.cs.
(יש כמובן לציין שבדוגמה בעולם האמיתי, היינו יוצרים את המחלקה ב-Class Library מחוץ לפרוייקט ה-Web שלנו)

image

קיבלנו את הקובץ הבא:

image

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

    public class WhiskeyBottle

    {

        public WhiskeyBottle(string Name, string PictureUrl, StorageLocation Storage)

        {

            this.Name = Name;

            this.PictureUrl = PictureUrl;

            this.Storage = Storage;

        }

 

        private string _name = string.Empty;

        public string Name

        {

            get { return _name; }

            set { _name = value; }

        }

 

 

        private string _pictureUrl = string.Empty;

        public string PictureUrl

        {

            get { return _pictureUrl; }

            set { _pictureUrl = value; }

        }

 

        private StorageLocation _storage = new StorageLocation();

        public StorageLocation Storage

        {

            get { return _storage; }

            set { _storage = value; }

        }

    }

אין פה הרבה - שלושה מאפיינים (גם באנגלית: Properties) וקונסטרקטור.
ניצור גם קובץ SotrageLocation.cs שיחזיק את מחלקת ה-StorageLocation.

    public class StorageLocation

    {

        public StorageLocation()

        {

        }

 

        public StorageLocation(string Name)

        {

            this.Name = Name;

        }

 

        private string _name = string.Empty;

        public string Name

        {

            get { return _name; }

            set { _name = value; }

        }

 

        public override string ToString()

        {

            return Name;

        }

    }

גם פה אין הרבה, מאפיין אחד, קונסטרקטורים ודרסנו את ToString שנצטרך אותו בהמשך.

 

נבנה ב-Code-Behind של הדף שלנו קצת קוד שייצר לנו נתונים לדוגמה (אשר לכאורה מגיעים מהמסד נתונים).

        public List<StorageLocation> GetStorageLocations()

        {

            List<StorageLocation> ReturnValues = new List<StorageLocation>();

 

            ReturnValues.Add(new StorageLocation("Bar"));

            ReturnValues.Add(new StorageLocation("Closet"));

            ReturnValues.Add(new StorageLocation("On the top shelf"));

            ReturnValues.Add(new StorageLocation("On the low shelf"));

 

            return ReturnValues;

        }

בנינו רשימה של StorageLocation וזאתי תהיה ה-DataSource ל-DropDownList שלנו.

        public List<WhiskeyBottle> GetWhiskeyBottle()

        {

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

 

            ReturnValues.Add(new WhiskeyBottle("Red Label", "~/bottles/RedLabel.jpg", GetStorageLocations()[0]));

            ReturnValues.Add(new WhiskeyBottle("Black Label", "~/bottles/BlackLabel.jpg", GetStorageLocations()[1]));

            ReturnValues.Add(new WhiskeyBottle("Green Label", "~/bottles/GreenLabel.jpg", GetStorageLocations()[1]));

            ReturnValues.Add(new WhiskeyBottle("Gold Label", "~/bottles/GoldLabel.jpg", GetStorageLocations()[2]));

            ReturnValues.Add(new WhiskeyBottle("Blue Label", "~/bottles/BlueLabel.jpg",GetStorageLocations()[3] ));

 

            return ReturnValues;

        }

כאן בנינו רשימה של בקבוקי וויסקי.

 

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

image

נוסיף את שם הבקבוק.

image

את המיקום שלו.

image

ואת התמונה שלו.

image

וזה הקוד ASP.Net שקיבלנו עד כה.

        <asp:GridView ID="grdBottles" runat="server" AutoGenerateColumns="False">

            <Columns>

                <asp:BoundField DataField="Name" HeaderText="Bottle Name"

                    SortExpression="Name" />

                <asp:BoundField DataField="Storage" HeaderText="Location"

                    SortExpression="Storage" />

                <asp:ImageField DataImageUrlField="PictureURL" DataImageUrlFormatString="{0}"

                    HeaderText="Picture">

                </asp:ImageField>

            </Columns>

        </asp:GridView>

(שימו לב בבקשה שהוספתי AutoGenerateColumns=False)

 

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

        protected void Page_Load(object sender, EventArgs e)

        {

            grdBottles.DataSource = GetWhiskeyBottle();

            grdBottles.DataBind();

        }

(זוכרים GetWhiskeyBottle מחזיר רשימת בקבוקי וויסקי ואנחנו כתבנו אותה)

 

כאשר נריץ נראה את הדף הבא.

image

 

 

עכשיו נגיע סוף-סוף לבעיה - איך עושים DataBinding ל-DropDownList בתוך GridView.
במקום ה-BoundField שמציג טקסט נרצה להוסיף DropDownList עם רשימת ערכים של StorageLocation.

                <asp:BoundField DataField="Storage" HeaderText="Location"

                    SortExpression="Storage" />

יהפוך ל:

                <asp:TemplateField HeaderText="Location" >

                    <ItemTemplate>

                        <asp:DropDownList ID="ddlLocations" runat="server"

                            DataSource=<%# GetStorageLocations() %>

                            DataTextField="Name" />

                    </ItemTemplate>

                </asp:TemplateField>

 

בואו נראה את זה בהרצה.

image

אז מה עשינו כאן? השתמשנו ב-Late Data Bound Expression ברמת הדף כדי להכניס נתונים לתוך ה-DropDownList.

אבל אפשר לראות משהו מעבר לזה, הנתון שכרגע נבחר ב-DropDownList הוא לא הנתון ברמת המחלקה.
אז נקבע שני מאפיינים ל-DropDownList:
1. לקחת את ה-Value של כל פריט ממהמאפיין Name.
2. לקבוע SelectedValue כ-WhiskeyBottle.Storage.Name.

נשתמש ב-Eval שקשור ל-DataBinding של ההורה כדי להשיג את סעיף 2.

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

                        <asp:DropDownList ID="ddlLocations" runat="server"

                            DataSource=<%# GetStorageLocations() %>

                            DataTextField="Name" />

והוא יהפוך ל:

    <asp:DropDownList ID="ddlLocations" runat="server"

                            DataSource=<%# GetStorageLocations() %>

                            DataTextField="Name"

                            DataValueField="Name"

                            SelectedValue=<%# Eval("Storage.Name") %>/>

 

נראה איך זה נראה בתצוגה.

image

 

בואו נעשה סדר בבלאגן איך עשינו את זה.

בשלב א' הוספנו GridView ואמרנו שהיא משתמשת ב-DataSource ברמת הדף שנקבע ב-Code Behind.

image

בשלב ב' הוספנו DropDownList שיש לה DataSource ברמת הדף ונקבע בתוך הדף ASP.Net עצמו.

image

בשלב ג' דאגנו שה-DropDownList שלנו תביט על הערך שה-GridView כרגע קבע ל-DataBinding על השורה.
זאת באמצעות ביטוי ה-Eval שקשור לקונטקסט הנוכחי שעובר DataBinding.

image

שימו לב ש-Eval הלך לרמת הקונסטקסט הנוכחי, וביטוי Data Bound אחר הלך ישר לרמת הדף להוציא נתונים.

 

נעשה צעד אחד נוסף ואחרון ונעבור מ-String Based Reference למאפיין ל-Strongly Typed Reference.

                        <asp:DropDownList ID="ddlLocations" runat="server"

                            DataSource=<%# GetStorageLocations() %>

                            DataTextField="Name"

                            DataValueField="Name"

                            SelectedValue=<%# Eval("Storage.Name") %>/>

יהפוך ל:

 <asp:DropDownList ID="DropDownList1" runat="server"

                            DataSource=<%# GetStorageLocations() %>

                            DataTextField="Name"

                            DataValueField="Name"

                            SelectedValue=<%# ((WebTesting_VS2008.WhiskeyBottle)Container.DataItem).Storage.Name %>/>

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

 

עוד על Late Data Bound Expressions ניתן לקרוא במאמר שלי משנת 2005:

Late Bound Data Expressions - הכוח שמאחורי הרעיון

 

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

 

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

Question from Tapuz .Net forum: Custom ASP.Net Calendar Control that Highlights Days

שאלה:

איך אפשר לצבוע ימים נבחרים בפקד Calendar?
ראיתי שיש אירוע DayRender ושם אפשר לצבוע ימים, זאת דרך טובה?

 

תשובה:

שאלה מצויינת שנותנת לנו לראות איך ירושה ב-Web Controls יכולה לתרום המון לבהירות של הקוד שלנו.

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

image

נוסיף את פקד ה-Calendar לדף:

  image

image

וככה זה יראה בדפדפן:

image

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

image

image

ובדפדפן:

image

עכשיו נרצה למשל להדגיש את ה-25.1.2008 כי יש לנו בו פגישה.
נראה איך עושים את זה ב-Event Handlers ואז נראה איך אפשר להפוך את הפתרון לכללי יותר דרך הורשה.

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

image

 

קיבלנו Event Handler לאירוע ה-DayRender בו נכתוב את הקוד שלנו.

        protected void Calendar1_DayRender(object sender, DayRenderEventArgs e)

        {

 

        }

עכשיו נבדוק אם התאריך הוא ה-25.1.2008 ואם כן נכתוב את הטקסט ב-Bold ונצבע את הרקע של התא.

        protected void Calendar1_DayRender(object sender, DayRenderEventArgs e)

        {

            if (e.Day.Date == new DateTime(2008, 1, 25))

            {

                e.Cell.Font.Bold = true;

                e.Cell.BackColor = Color.LightCoral;

            }

        }

נריץ את זה בדפדפן.

image

ובאמת אפשר לראות שה-25.1.2008 נצבע באדום והפך להיות Bold.

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

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

        protected void Page_Load(object sender, EventArgs e)

        {

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 14));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 15));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 25));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 29));

        }

וככה תיראה התוצאה:

image

נתחיל בליצור פרוייקט מסוג Web Control Library, או ב-Visual Studio 2008 לפרוייקט קוראים ASP.Net Server Control.

image

וזה מה שקיבלנו ב-Solution Explorer.

image

נמחק את ServerControl1.cs (כי הוא לא קשור) וניצור קובץ חדש בשם HighlightedCalendar.

image

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

image

image

OK, דבר ראשון שנרצה לעשות זה להפוך את המחלקה ל-Public כדי שהפרוייקט Web שלנו יוכל לראות אותה ולדאוג שהיא יורשת מ-Calendar של הפריימוורק.

using System.Web.UI.WebControls;

 

namespace WebControlLibrary_VS2008

{

    public class HighlightedCalendar : Calendar

    {

    }

}

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

image

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

נתחיל ברשימת התאריכים להדגיש.

        private List<DateTime> _highlightedDates = new List<DateTime>();

 

        public List<DateTime> HighlightedDates

        {

            get { return _highlightedDates; }

            set { _highlightedDates = value; }

        }

וסגנון התא להדגשה.

        private TableItemStyle _highlightedDayStyle = new TableItemStyle();

 

        public TableItemStyle HighlightedDayStyle

        {

            get { return _highlightedDayStyle; }

            set { _highlightedDayStyle = value; }

        }

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

        public HighlightedCalendar()

        {

            this.DayRender += new DayRenderEventHandler(HighlightedCalendar_DayRender);  

        }

 

        void HighlightedCalendar_DayRender(object sender, DayRenderEventArgs e)

        {

 

        }

נבצע את הבדיקה האם התאריך נמצא ברשית התאריכים להדגשה. רק קודם נראה את ה-Intellisense על ה-EventArgs באירוע.

image

        void HighlightedCalendar_DayRender(object sender, DayRenderEventArgs e)

        {

            if (HighlightedDates.Contains(e.Day.Date))

            {

 

            }

        }

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

        void HighlightedCalendar_DayRender(object sender, DayRenderEventArgs e)

        {

            if (HighlightedDates.Contains(e.Day.Date))

            {

                ApplyStyleToCell(e.Cell, this.HighlightedDayStyle);

            }

        }

חשוב להדגיש שהפונקציה ApplyStyleToCell עדיין לא קיימת ונצטרך ליצור אותה בעצמנו.

image

קיבלנו את:

        private void ApplyStyleToCell(TableCell tableCell, TableItemStyle tableItemStyle)

        {

 

        }

נמלא אותו בקוד שדואג להעתיק מידע מה-Style וה-Font לתוך התא הנוכחי.

        private void ApplyStyleToCell(TableCell cellToColor, TableItemStyle styleToApply)

        {

            cellToColor.BackColor = styleToApply.BackColor;

            cellToColor.BorderColor = styleToApply.BorderColor;

            cellToColor.BorderStyle = styleToApply.BorderStyle;

            cellToColor.BorderWidth = styleToApply.BorderWidth;

            cellToColor.CssClass = styleToApply.CssClass;

            cellToColor.ForeColor = styleToApply.ForeColor;

            cellToColor.Height = styleToApply.Height;

            cellToColor.HorizontalAlign = styleToApply.HorizontalAlign;

            cellToColor.VerticalAlign = styleToApply.VerticalAlign;

            cellToColor.Width = styleToApply.Width;

            cellToColor.Wrap = styleToApply.Wrap;

            cellToColor.Font.CopyFrom(styleToApply.Font);

        }

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

image

נלחץ כפתור ימני ונבחר Refactor --> Extract Method.

image

image

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

        void HighlightedCalendar_DayRender(object sender, DayRenderEventArgs e)

        {

            if (IsHighlightDate(e))

            {

                ApplyStyleToCell(e.Cell, this.HighlightedDayStyle);

            }

        }

 

        private bool IsHighlightDate(DayRenderEventArgs e)

        {

            return HighlightedDates.Contains(e.Day.Date);

        }

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

 

בשלב זה נרצה להוסיף Reference בין הפרוייקט Web שלנו ל-Web Control Library.

image

image

נחזור לטופס ASP.Net שלנו ועכשיו נראה את הפקד החדש בצד שמאל ב-Toolbox.
נזרוק אותו על הטופס.

image

image

נעשה לו אותו Auto-Format שעשינו לפקד Calendar מעליו.
image

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

image

נרצה לעשות כמה שינויים במאפיינים הציבוריים שחשפנו.
הראשון, נרצה להעלים את HighlightedDates בכלל מה-Properties ככה שנוכל לקבוע אותו רק מה-Code Behind.

        private List<DateTime> _highlightedDates = new List<DateTime>();

        [Browsable(false), DefaultValue((List<DateTime>) null)]

        public List<DateTime> HighlightedDates

        {

            get { return _highlightedDates; }

            set { _highlightedDates = value; }

        }

השני, נרצה ש-HighlightedDayStyle ישב עם כל שאר החברים שלו ב-Styles.

        private TableItemStyle _highlightedDayStyle = new TableItemStyle();

        [PersistenceMode(PersistenceMode.InnerProperty),

        DesignerSerializationVisibility(DesignerSerializationVisibility.Content),

        DefaultValue((string) null),

        Category("Styles")]

        public TableItemStyle HighlightedDayStyle

        {

            get { return _highlightedDayStyle; }

            set { _highlightedDayStyle = value; }

        }

שאר הקוד מיועד כדי לגרום ל-Designer לתת ערכי Default למאפיינים שיצרנו, ולדאוג לשמור את ה-Style בתגית פנימית מקוננת ב-ASP.Net.

נראה איך זה נראה ב-Designer.

image

העלמנו לחלוטין את SelectedDates מה-Designer והוספנו את HighlightedDayStyle לקטגוריית Styles.
עכשיו נרצה לשנות כמה מאפיינים בתצוגה, למשל לקבוע צבע רקע ולקבוע שה-Font הוא Bold.

 image

ב-Code Behind שלנו נפרט איזה ימים נרצה שיודגשו.

        protected void Page_Load(object sender, EventArgs e)

        {

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 14));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 15));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 25));

            HighlightedCalendar1.HighlightedDates.Add(new DateTime(2008, 1, 29));

        }

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

image

 

הקוד של הפקד

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Text;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace WebControlLibrary_VS2008

{

    public class HighlightedCalendar : Calendar

    {

        public HighlightedCalendar()

        {

            this.DayRender += new DayRenderEventHandler(HighlightedCalendar_DayRender);  

        }

 

        void HighlightedCalendar_DayRender(object sender, DayRenderEventArgs e)

        {

            if (IsHighlightDate(e))

            {

                ApplyStyleToCell(e.Cell, this.HighlightedDayStyle);

            }

        }

 

        private bool IsHighlightDate(DayRenderEventArgs e)

        {

            return HighlightedDates.Contains(e.Day.Date);

        }

 

        private void ApplyStyleToCell(TableCell cellToColor, TableItemStyle styleToApply)

        {

            cellToColor.BackColor = styleToApply.BackColor;

            cellToColor.BorderColor = styleToApply.BorderColor;

            cellToColor.BorderStyle = styleToApply.BorderStyle;

            cellToColor.BorderWidth = styleToApply.BorderWidth;

            cellToColor.CssClass = styleToApply.CssClass;

            cellToColor.ForeColor = styleToApply.ForeColor;

            cellToColor.Height = styleToApply.Height;

            cellToColor.HorizontalAlign = styleToApply.HorizontalAlign;

            cellToColor.VerticalAlign = styleToApply.VerticalAlign;

            cellToColor.Width = styleToApply.Width;

            cellToColor.Wrap = styleToApply.Wrap;

            cellToColor.Font.CopyFrom(styleToApply.Font);

        }

 

        private TableItemStyle _highlightedDayStyle = new TableItemStyle();

        [PersistenceMode(PersistenceMode.InnerProperty),

        DesignerSerializationVisibility(DesignerSerializationVisibility.Content),

        DefaultValue((string) null),

        Category("Styles")]

        public TableItemStyle HighlightedDayStyle

        {

            get { return _highlightedDayStyle; }

            set { _highlightedDayStyle = value; }

        }

 

 

        private List<DateTime> _highlightedDates = new List<DateTime>();

        [Browsable(false), DefaultValue((List<DateTime>) null)]

        public List<DateTime> HighlightedDates

        {

            get { return _highlightedDates; }

            set { _highlightedDates = value; }

        }

    }

}

 

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

בנוסף, ראינו בקצרה איך כותבים Custom Control בסיסי.

 

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

 

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

חוויות של יועץ עצמאי בתחום הדוט נט - חיוב יתר

החיים שלי היו הרבה יותר פשוטים לפני שנה.

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

בפוסט הזה ובתקווה באחרים שיבואו אחריו אפרט על החוויות שלי כ*תופים* יועץ Net. בכיר *תופים*. (יש לקרוא עם הד דרמטי: בכי, כיר, יר, ר)

Consulting - If You're Not A Part Of The Solution, There's Good Money To Be Made In Prolonging The Problem

If You're Not A Part Of The Solution,
There's Good Money To Be Made In Prolonging The Problem

 

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

 

יאללה, פשוט תחייב אותם על יותר שעות

נתחיל בגילוי נאות, אני אולי היועץ העצמאי בארץ הכי יקר בתחום הדוט נט שאני מכיר.
לשעת יעוץ אני לוקח 100$ מזומן, ורק ברכישה מראש של 50 שעות או יותר אני לוקח 90$ לשעה.

זה עם בערך 150 שעות יעוץ בחודש, וחופש כל שבוע ליום שלישי הקדוש.

 

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

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

 

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

image

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

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

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

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

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

image

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

דוגמה לאיך חיוב שלי נראה:

image

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

 

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

 

Visual Studio 2008

Question from Tapuz .Net forum: Copy to Output directory, Build Action And Custom Tool

שאלה:

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

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

 

תשובה:

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

בואו נפתח פרוייקט Console חדש.

image

נראה מה קיבלנו ב-Solution Explorer:

image

קיבלנו קובץ בשם Program.cs שכמו שאנחנו יודעים, מכיל את נקודת הכניסה (Main) של התוכנית.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace ConsoleResources

{

    class Program

    {

        static void Main(string[] args)

        {

        }

    }

}

נוכל גם לראות משהו מעניין שהשארתי בכוונה - יש לנו גישה לחלונית ה-Properties של הקובץ.

image

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

image

יש לנו File Name ו-Full Path שאומרים לנו מה שכבר ידענו - יש קובץ בשם Program.cs בנתיב מסויים.

image

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

image

נזכיר שאנחנו מביטים על קובץ #C ולכן הדבר ההגיוני לעשות כאשר נתחיל תהליך Build יהיה לקמפל אותו.
ולכן יש לנו BuildAction = Compile לקובץ ה-cs שלנו.

בואו נראה איזה אפשרויות נוספות יש:

image

 

נעבור לפי דוגמה מה כל אחד מהסוגים האלו.
* Compile - ראינו שהקובץ program.cs שלנו הוא Compile. כלומר, מדובר בקבצים שבסופו של דבר מגיעים לאחד מהקומפיילרים הדוט-נטיים.
* Content - אם למשל נפתח ASP.Net WebSite מרבית הקבצים הם הרי לא עוברים קומפילציה עד הרגע ולכן מרבית הקבצים יהיו Content.

image image image

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

כמובן ש-Default.aspx.cs הוא כן מסוג Compile היות והוא מגיע לאחד הקומפיילרים הדוט-נטיים במהלך Build.

* Page, ApplicationDefinition, Resource - כל השלושה קשורים לטכנולוגיית WPF שהיא חלק מדוט נט 3.0. בקצרה, ApplicaitonDefinition הוא הנקודת כניסה מסוג XAML הראשונה באפליקציה, Page הוא חלון כלשהו ו-Resource הוא תחליף WPFי ל-Embedded Resource. כמו כן, יש גם SilverlightPage שהוא המקביל ל-Page ב-Silverlight.

* CodeAnalysisDictionary - חלק ממנגנון ה-Spell Checking החדש ב-Visual Studio 2008. בקובץ ה-XML הזה נגדיר מילים וביטויים שיעברו Spell Checking.

* Embedded Resource - קובץ שהופך להיות חלק מהאסבלי במהלך קמפול. נדבר עליו בהרחבה בהמשך.

 

אז כפי שהבנו ישנם שלושה סוגי Build Action מעניינים: Content, Compile ו-Embedded Resource.
כאשר כל השאר הם Extensions למטרות ספציפיות כלשהן.

 

עכשיו נדבר על Copy To Output Directory ונענה לאחת השאלות המקוריות.
נרצה להוסיף לפרוייקט קובץ TXT ולפתוח אותו באמצעות Notepad שהתוכנית עולה.

image

image

ועכשיו יש לנו בפרוייקט קובץ TXT חדש.

image

image

נביט על המאפיינים שלו.

image

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

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

 

namespace ConsoleResources

{

    class Program

    {

        static void Main(string[] args)

        {

            Process.Start(

                @"C:\Documents and Settings\Justin-Josef Angel\My Documents\Visual Studio 2008\Projects\ConsoleResources\myText.txt");

 

            Console.ReadLine();

        }

    }

}

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

image

 

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

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

אז נכתוב נתיב חלקי:

        static void Main(string[] args)

        {

            Process.Start("myText.txt");

 

            Console.ReadLine();

        }

וקיבלנו את השגיאה הבאה:

image 

דוט נט אומרת לנו שהיא לא הצליחה למצוא את הקובץ המבוקש.
כברירת מחדל הנתיב ש-Process.Start מחפש הוא בתיקייה של ה-exe ובכמה תיקיות System ו-Windows.

אז נרצה להעתיק את הקובץ TXT שלנו לתיקייה בה הקומפילציה שמה את הקובץ EXE שלנו.

כלומר, זוהי תיקיית הפרוייקט שלנו:

image

ובתוך Bin\Debug נמצא את הקובץ EXE שלנו.

image

(הערה צדדית. למי שתוהה, הקובץ vshost הכרחי כדי ש-Visual Studio יוכל לעשות Attach ולתת לנו יכולות Debugging. הקובץ PDB הוא הקובץ שמאפשר ל-Visual Studio להכיר את מבנה הקוד הפנימי שלנו).

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

אז ניכנס למאפיינים שלו ונראה מה אפשר לקבוע תחת Copy To Output Directory.

image

מהפירוט על Copy to Output Directory נראה שכאן נקבע את צורת ההעתקה לספריית היעד.
ויש שלושה אפשרויות: לא להעתיק (שזאת הברירת מחדל לכל קבצי ה-Content), תמיד להעתיק ולהעתיק רק אם יש שינוי.

נבחר את Copy Always.

image

נקמפל את האפליקציה (בלי להריץ עדיין).

image

ונראה שב-Bin\debug שלנו נמצא הקובץ TXT.

image

 

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

image

כלומר, כדי להריץ באמצעות Process.Start עם שם קובץ בלבד, מספיק לקבוע Copy To Output Directory.

 

עכשיו נדבר על Embedded Resources אם כבר אנחנו על נושא שלושת ה-Build Action.

נפתח אפליקציית Winforms חדשה.

image

וקיבלנו את האפליקציה הבסיסית הבאה:

image

image

נוסיף לפרוייקט שלנו תמונה.

image

נוסיף תמונה של בקבוקי הוויסקי שלי.

image

נביט על הקובץ ב-Solution Explorer.

image

כלומר, קובץ רגיל מסוג Content.

נוסיף לטופס הראשי של האפליקציה PictureBox ונדאג שתצביע לנתיב המלא של התמונה.

image

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

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

image

עכשיו נרצה לעשות אותו טריק מקודם בכדי להוריד את הנתיב המלא.
נקבע Copy To Output Directory = Copy If Newer.

image

ונשנה את ה-PictureBox.ImageLocation לשם הקובץ בלבד.

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

image

אבל רגע, בואו נביט בספריית Bin\Debug שלנו.

image

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

סה"כ לא טוב.

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

נשנה את ה-Build Action של התמונה ל-Embedded Resource, וגם נגיד שלא צריך להעתיק אותה ל-Bin שלנו.

image

עכשיו נקמפל את האפליקציה.

דבר ראשון, אין יותר קובץ תמונה ב-Bin.

image 

נפתח את ה-EXE שלנו ב-Reflector.

image

ואם נתחיל לדפדף בתוך Resources של האסמבלי הזו, נמצא את התמונה שלנו.

image

נבקש לראות את התמונה בתוך Reflector.

image

וכן יש מולנו בקבוק וויסקי ענק.

image

 

כלומר, הקובץ תמונה לא ב-Bin שלנו.
הקובץ Embedded Resource הוא חלק מהאסבלי שלנו!

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

        private void Form1_Load(object sender, EventArgs e)

        {

            using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream("WinformsResources.100_2116.JPG"))

            {

                pictureBox1.Image = Image.FromStream(s);

            }

        }

סה"כ אמרנו לאמסבלי הנוכחית שלנו שמחזיקה את הקובץ ב-Resources שלה להביא לנו קובץ עם שם מסויים, להמיר אותו לתמונה ולהכניס לתוך ה-PictureBox.Image שלנו.

נריץ את האפליקציה, ונראה שהתמונה מוצגת.

image

 

כלומר, האפליקציה ניגשה ל-Resource בתוך האסמבלי לפי שם קובץ, הוציא החוצה כרצף בינארי והפכה לתמונה.

 

ניקח את זה צעד קדימה ונראה איך אפשר לעשות את זה אוטומטית יותר.
נמחק את התמונה מהאפליקציה שלנו.

image

עכשיו נפתח את תגית ה-Properties של הפרוייקט ונראה שמסתתר שם קובץ בשם Resources.resx.

image

לחיצה כפולה עליו ונגיע למסך הבא.

image

נבחר Add Resource --> Existing Resource.

image

והפעם נוסיף תמונה של האקדח שלי.

image

נוכל לראות שהתמונה התווספה כ-Image Resource לפרוייקט.

image

וב-Solution Explorer נוכל לראות שהקובץ חלק מהפרוייקט כ-Embedded Resource.

image

 

נחזור לטופס שלנו ונרצה תכנותית לקבוע תמונה ל-PictureBox.

image

        private void Form1_Load(object sender, EventArgs e)

        {

            pictureBox1.Image = Resources._100_2115;

        }

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

image

כלומר לקבצים שנוסיף דרך ממשק ה-Resources נקבל גישה שהיא Strongly-typed. נחזור עוד מעט לאיך זה קורה שנדבר על Custom Tool.

 

עכשיו נלך על משהו עו יותר פשוט ובלי קוד בכלל. נמחק את הקוד ב-Form_Load שלנו.
נחזור ל-Designer ונלחץ על השלוש נקודות ב-Image.

image

 

וכאן נוכל לבחור את ה-Image לפי מה שהוספנו בקובץ Resources שלנו.

image

וככה זה נראה ב-Designer:

image

 

אם נפתח את Reflector נוכל לראות שהתמונה שלנו שהוספנו דרך Resources מופיעה ב-Resources של האסמבלי.

image 

 

נעבור לדבר על Custom Tool בקצרה.
שנפרט Custom Tool לקובץ מדובר על להריץ תוכנה קטנה כל פעם שהקובץ משתנה.

למשל, בואו נדבר על איך ה-Resources שלנו קיבל ל-Intellisense את התמונה שהוספנו.
נוכל לראות שתחת Resources.resx יש קובץ בשם Resources.designer.cs.

image

נפתח את הקובץ ונביט עליו.

image

סיננתי חלק מהקובץ שיהיה נוח לצפייה, אבל אפשר לראות שני דברים מעניינים:
1. אזהרה ענקית למעלה לא לגעת בקובץ כי הקובץ עבר ג'נרונט (מלשון To Generate) אוטומטי.
2. שיש אובייקט בשם 2115_100 שמממש כתוב בקוד.

רגע, אנחנו לא הוספנו את האובייקט הזה לקוד!
הרי, הוא התווסף אוטומטית ע"י ג'נרונט הקוד.

 

אז בואו נביט לרגע על המאפיינים של Resources.resx.

image

אפשר לראות שהקובץ עצמו הוא Embedded Resource, אבל יותר מזה - יש לו Custom Tool.
ה-Custom Tool הזה רץ הרי כל פעם שהקובץ Resources.resx משתנה.

אז אנחנו רואים כאן רצף עניינים.
1. אנחנו עורכים ב- Resources Designer את המשאבים.

image

2. אלו מתווספים לקובץ ה-Resources.resx שהוא סה"כ XML.

image

3. היות וקובץ ה-Resources.resx השתנה ה-Custom Tool שלו רץ.

image

4. נקבל בסוף קובץ בשם Resources.designer.cs שיוצר ע"י הכלי האוטומטי ResXFileCodeGenerator.

image

 

יש עוד כמה Custom Tools ב-Visual Studio. למשל: MSDiscoGenerator שאחראי על יצור WebService Proxy מחדש כל פעם שה-Webservice משתנה או ה-URL אליו משתנה, ו-MSLinqToSqlGenerator שאחראי לג'נרנט מחדש מחלקות שקובץ dmbl של Linq To Sql משתנה.
אם אתם לא מכירים אותם, זה לא משנה, הקונספט הוא זה שחשוב.

image

 

נסכם על מה עברנו במאמר זה.

1. Build Action.

image

Content כברירת מחדל לא מעניינת, Compile שעובר קומפילציה, ו-Embedded Resource שנכנס כקובץ לתוך האסמבלי.

 

2. Copy To Out Directory שדואג להעתיק קובץ לספריית ה-bin.

image

 

3. Custom Tool שמג'נרנט קוד אחרי שהקובץ השתנה.

image

 

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

Question from Tapuz .Net forum: Extracting data from an HTML File

שאלה:

מה הדרך הנכונה לעבודה מול תוכן XHTML?
לדוגמה אני טוען תכן xhtml מקובץ ורוצה לבצע בו מספר שינויים לפני שאני מציג אותו ללקוח.
מה שאני עושה כיום זה טוען אותו כxml ומשתמש במחלקות ממשפחת XmlDocument וXDocument, ויש בזה הרבה חסרונות. יש דרך טובה יותר? (Net 3.5.)

 

תשובה:

יש כמה אפשרויות, אבל אין ביניהן דרך טובה.

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

אופציה א' היא לעבוד איתו "כאילו" הוא XML.
למה "כאילו" כי HTML הוא 100% מהזמן לא XML תקני ויש תגיות של נסגרות שם למשל.
אז שלב א' הוא תמיד ביחס למסמך הספציפית מאוד שאתה צריך לטעון במסגרת התוכנה לדאוג לתקן אותו. השילוב הוא לרוב בין Replaceים רגילים של Stringים (למשל <br> להחליף ב-<br /> עם סוגר) ושימוש ב-Replaceים של Regexpים.
הדרך הכי טובה לגלות מה הבעיה עם HTML ספציפי כלשהו הוא פשוט לטעון אותו ולהתחיל לתקן עד שכל השגיאות נעלמות.

אחרי שהבאת את ה-HTML למצב של XML תקני, תבצע את שלב ב' שבו תטען אותו ל-XmlDocument ותבצע ביטויי XPath.
אני משתמש ב-SketchPath כדי להכניס לתוכו מסמך XML תקני והוא באמצעים גרפים דואג לרשום עבורי את הביטוי XPath הבסיסי.

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

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

פעם שקלתי לבסס על זה Custom Workflow Element ב-Workflow Foundation שיהיו סה"כ לבני לגו שזורקים על טופס לפי הסדר שמצפים לאלמנטים להופיע ושמוצאים אלמנטיים ספציפיים אפשר לעלות אירועים.

 

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

Question from Tapuz .Net Forum: Waiting for a process to close

שאלה:

אני צריך להפעיל תוכנה חיצונית מהקוד שלי, כך ש: 1- החלון יופיע מעל החלון שלי 2- חלון האב לא יהיה זמין כל עוד פתוח החלון השני משהו כמו ההתנהגות של MessageBox. איך אפשר לעשות כזה דבר?

 

תשובה:

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

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

        public const string myProcessName = "myProcess.exe";

        static void Main(string[] args)

        {

 

            Process.Start(myProcessName);

            Process[] myProcs = Process.GetProcessesByName(myProcessName);

            if (myProcs.Length != 1)

                throw new ApplicationException("Exactly one process should be alive! ");

            myProcs[0].WaitForExit();

        }

הקוד לא מסובך מדי, אבל נעבור עליו שורה-שורה.

נרשום את השם של ה-EXE שנרצה להתחיל.

        public const string myProcessName = "myProcess.exe";

נריץ את ה-EXE.

            Process.Start(myProcessName);

נבקש מה-OS שלנו את רשימת ה-Processים עם אותו שם של Process כמו התוכנית שכרגע פתחנו.

            Process[] myProcs = Process.GetProcessesByName(myProcessName);

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

            if (myProcs.Length != 1)

                throw new ApplicationException("Exactly one process should be alive! ");

אגב, שווה לשים לב כאן ל-Best Practice בדוט נט.
אם זורקים חריגות (גם באנגלית: Exception) מהקוד שלנו, תמיד עדיף לזרוק ApplicationException או חריגות שלנו שיורשות ממנו.
זאת כדי להבדיל בין שגיאות שאנחנו מעלים לבין שגיאות של הפריימוורק.

ולבסוף נשתמש במתודת הקסם WaitForExit.

            myProcs[0].WaitForExit();

 

מתוך התיעוד על הפונקציה Process.WaitForExit:

image

image

 

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

Question from Tapuz .Net forum: Linq to SQL OnValidate partial method doesn't fire

שאלה:

יצרתי partial class עם שם הטבלה שלי, product במקרה זה, ואז 
implements the OnValidate() partial method
ושם כתבתי את החוקים שלי לפי הצורך.
שמתי BreakPoint בתוך OnValidate ואני רואה שהיא לא נפגעת.
כלומר, בזמן שמירה של השורה, המתודה שלי לא נקראת, ולכן כל העסק לא עובד.
האם יש איזה רעיון למה?

    public partial class Product

    {

        public void OnValidate()

        {

            if ((this.Discontinued == true) && (this.UnitsOnOrder > 0))

            {

                throw new ArgumentException("Reorder level cannot be greater then 0 when you discontinue");

            }

        }

    }

 

תשובה:

בוא נבין קודם מה זה כל ה-Partial הזה.

בואו למשל נביט על טופס Winform.

image

על הטופס הזה שמנו כל מיני פקדים גרפיים, למשל Button ו-TextBox.

image

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

בדוט נט 1.1 הייתה לנו מתודה בשם InitializeCompoenent שישבה בתוך Form1.cs וה-Designer הכניס לשם עוד הרבה קוד אוטומטי.

image

נעבור על זה חלק-חלק.

image

כאן יש לנו Constructor שיוצר ע"י ה-Designer וזה דואג לקרוא למתודה InitializeComponent.

image

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

image

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

        /// <summary>

        /// Clean up any resources being used.

        /// </summary>

        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>

        protected override void Dispose(bool disposing)

        {

            if (disposing && (components != null))

            {

                components.Dispose();

            }

            base.Dispose(disposing);

        }

עכשיו נביט על דברים שהם קצת פחות "תשתית" וקצת יותר ברור לנו מה הם עושים בפועל:

image

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

ככה למשל היה אפשר לעשות:

image

image

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

פקד גרפי --> משתנה ברמת הטופס.

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

image

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

image

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

image

קודם הכפתור.

image

ואחר כך ה-TextBox שלנו.

 

אז כמו שאמרנו, כל זה היה עוד בדוט נט 1.1 וב-Visual Studio 2003.

בדוט נט 2.0 וב-Visual Studio 2005 הבינו מיקרוסופט שמדובר בהמון קוד מג'ונרנט אוטומטית שנמצא בתוך הטופס שלנו וזה מקשה עלינו כמתכנתים לעבוד.

אז המציאו את ה-Partial Classes כחלק מ-C# 2.0 ו-VB.Net 2.0.
הקונספט הוא לאפשר לכתוב את אותה מחלקה (גם באנגלית: Class) בשניים או יותר קבצים שמתקמפלים ביחד.

למשל, בנושא ה-Designer של winform, זה מאפשר את המבנה הבא:

image

כאן, יש לנו שני קבצי CS שכל אחד מהם מכיל "חלק מהמחלקה Form1".

ב-VS2005 דאגו "להגלות" את הקוד המג'ורנט הרחק מאתנו לתוך קובץ ה-designer.cs (שהוא קובץ CS לכל דבר).

 

בקובץ הראשי שלנו השאירו רק את המינימום קוד הנדרש.

 image

שימו לב ש-Form1 הוא Partial Class עכשיו ולא Class רגיל.
כלומר, קובץ אחר בתוך אותו פרוייקט יכול גם להכיל קוד בשביל Form1.

ובאמת ב-Form1.designer.cs יש את ההצהרה הבאה למעלה:

image

וכל הקוד שג'ונרנט (מלשון To Generate) עבורנו, נמצא עכשיו בקובץ הצדדי הזה.

 

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

הקונספט מאחורי Partial Method מאפשר להצהיר על Partial Method בקובץ X1.cs, לקרוא לה בקובץ X1.cs ולממש אותה רק בקובץ X2.cs.
כמובן שזה היה אפשרי כבר קודם (לקרוא למתודות מ-Partial Class של המחלקה שאתה כרגע נמצא בה).
החידוש כאן הוא שאם ה-Partial Method לא ממושת ב-Partial class שלך, הקריאה לפונקציה פשוט לא מתקמפלת .

בואו נדגים את זה באמצעות Linq To Sql. 

image

יש כאן דיאגרמת Linq to Sql עם מחלקה בשם Address שמקושרת לטבלת Address במסד הנתונים.

מאחורי הקלעים, יש לנו שלושה קבצים:

image

קובץ ה-DBML מכיל את ההגדרות שמחברות בין Classes לבין טבלאות במסד הנתונים (זהו קובץ ה-ORM שלנו, ה-Object-To-Relational mapping).
יש את קובץ ה-layout שמכיל כל מיני פרטים על הסידור הגרפי שראינו למעלה (קורדינטות X,Y של כל טבלה על משטח העיצוב, מה פתוח ומה לא וכך הלאה). בעקרון, שום מידע שימושי מלבד ל-Designer הגרפי.

ויש לנו את קובץ ה-designer.cs שמיוצר אוטומטית מקובץ ה-DBML עבורנו ע"י ה-MSLinqToSqlGenerator.

נביט קצת לתוך הקובץ הזה:

image

אפשר לראות שבין השאר יש בקובץ הזה Partial Class בשם Address.

בתוך ה-Partial Class הזו יש גם כמה Partial Methods.

    [Table(Name="Person.Address")]

    public partial class Address : INotifyPropertyChanging, INotifyPropertyChanged

    {

     #region Extensibility Method Definitions

    partial void OnLoaded();

    partial void OnValidate(System.Data.Linq.ChangeAction action);

    partial void OnCreated();

    partial void OnAddressIDChanging(int value);

    partial void OnAddressIDChanged();

    partial void OnAddressLine1Changing(string value);

    partial void OnAddressLine1Changed();

    partial void OnAddressLine2Changing(string value);

    partial void OnAddressLine2Changed();

    partial void OnCityChanging(string value);

    partial void OnCityChanged();

    partial void OnStateProvinceIDChanging(int value);

    partial void OnStateProvinceIDChanged();

    partial void OnPostalCodeChanging(string value);

    partial void OnPostalCodeChanged();

    partial void OnrowguidChanging(System.Guid value);

    partial void OnrowguidChanged();

    partial void OnModifiedDateChanging(System.DateTime value);

    partial void OnModifiedDateChanged();

    #endregion

נביט למשל על המאפיין (גם באנגלית: Property) שנקרא City.

        [Column(Storage="_City", DbType="NVarChar(30) NOT NULL", CanBeNull=false)]

        public string City

        {

            get

            {

                return this._City;

            }

            set

            {

                if ((this._City != value))

                {

                   this.OnCityChanging(value);

                    this.SendPropertyChanging();

                    this._City = value;

                    this.SendPropertyChanged("City");

                   this.OnCityChanged();

                }

            }

        }

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

עכשיו לעניינו, אפשר לראות קריאה לשתי Partial Methods במאפיין הזה: OnCityChanging ו-OnCityChanged.
אלו הן חלק מה-Partial Methods של ה-Partial Class שלנו.

 

    [Table(Name="Person.Address")]

    public partial class Address : INotifyPropertyChanging, INotifyPropertyChanged

    {

     #region Extensibility Method Definitions

    partial void OnLoaded();

    partial void OnValidate(System.Data.Linq.ChangeAction action);

    partial void OnCreated();

    partial void OnAddressIDChanging(int value);

    partial void OnAddressIDChanged();

    partial void OnAddressLine1Changing(string value);

    partial void OnAddressLine1Changed();

    partial void OnAddressLine2Changing(string value);

    partial void OnAddressLine2Changed();

    partial void OnCityChanging(string value);

    partial void OnCityChanged();

    partial void OnStateProvinceIDChanging(int value);

    partial void OnStateProvinceIDChanged();

    partial void OnPostalCodeChanging(string value);

    partial void OnPostalCodeChanged();

    partial void OnrowguidChanging(System.Guid value);

    partial void OnrowguidChanged();

    partial void OnModifiedDateChanging(System.DateTime value);

    partial void OnModifiedDateChanged();

    #endregion

כלומר, בקובץ designer.cs שלנו יש הצהרה גם לשם של ה-Partial Method וגם קריאה עבורה.
image  - הצהרה + קריאה.

היות ואין שום מימוש ב-Partial Class אחר ל-Partial Methods האלו - ההצהרה והקריאה להן פשוט יעלמו בקומפילציה.
נפתח ב-Reflector ונביט על המחלקה המקומפלת:

image

 

image

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

והכל - כי אין להן מימוש.

עכשיו, נממש את הפונקציות האלו ב-Partial Class.

image

    partial class Address

    {

        partial void OnCityChanged()

        {

            // do something

        }

 

        partial void OnCityChanging(string value)

        {

            // do something else

        }

    }

נקמפל שוב את המחלקה שלנו, ונבדוק ב-Reflector איך היא נראית איך קומפילציה.

image

 

image

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

 

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

    public partial class Product

    {

        public void OnValidate()

        {

            if ((this.Discontinued == true) && (this.UnitsOnOrder > 0))

            {

                throw new ArgumentException("Reorder level cannot be greater then 0 when you discontinue");

            }

        }

    }

דבר ראשון חשוב לשים לב שחסרה לנו מילת המפתח Partial בשביל להפוך את המתודה ל-Partial Method.

    public partial class Product

    {

        partial void OnValidate()

        {

            if ((this.Discontinued == true) && (this.UnitsOnOrder > 0))

            {

                throw new ArgumentException("Reorder level cannot be greater then 0 when you discontinue");

            }

        }

    }

 

דבר שני, נביט למשל במחלקת ה-Address שלנו על ההצהרה של OnValidate.

    partial void OnValidate(System.Data.Linq.ChangeAction action);

אנחנו יודעים שצריך שני דברים בקובץ שמצהיר Partial Method: הצהרה וקריאה.

אבל בקובץ המג'ורנט שלנו אין שום קריאה ל-OnValidate!

נביט על תמונת מסך של ה-Partial Methods:

image

אני משתמש בתוסף ל-Visual Studio 2008 בשם Resharper, שנכון לעכשיו לא תומך בצורה ב-Partial Methods וצובע הכל באטרף באדום.
נתעלם מכל האדום, ונחפש קצת אפור.

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

כלומר, אנחנו צריכים ב-Partial Class שלנו גם לקרוא ל-OnValidate וגם לממש אותו.

    partial class Address

    {

        partial void OnCityChanged()

        {

            OnValidate();

        }

 

        partial void OnCityChanging(string value)

        {

            // do something else

        }

 

        partial void OnValidate(System.Data.Linq.ChangeAction action)

        {

            // do validation

        }

    }

 

עכשיו קצת ביקורת.

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

לעוד על VAB אפשר לקרוא כאן סדרת מאמרים שכתבתי עליו - http://www.justinangel.net/#Validation Application Block.

 

דבר שני, על Partial Methods בכלל.

המקום היחידי שבו כזה דבר באמת מתאים זה באמת איפה ש-Partial Classes עזר, רק שיש לך Designer שמגביל אותך מאוד במימוש.
אסור תחת שום תנאי לעבוד עם Partial Methods ככלי עבודה יום-יומי במפתחים.

הסיבה היא שיש לנו כבר כלים תכנותיים שיותר מתאימים למצבים כאלו, ספציפית abstract methods וירושה.

    public abstract class myEvening

    {

        public void Relex()

        {

            DrinkWhiskey();

        }

 

        protected abstract void DrinkWhiskey();

    }

 

    public class JustinsEvening : myEvening

    {

        protected override void DrinkWhiskey()

        {

            Justin.Drink(BlakcLabel);

        }

    }

ירושה מוכרת ומבוססת ככלי עבודה חזק ואין לו את המגבלות של Partial Methods, ולכן תמיד עדיף לעבוד איתו.

 

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