Justin Angel's Silverlight Weblog

This blog has moved to a Silverlight powered weblog on @ http://JustinAngel.net

Read the Silverlight weblog features overview at @ http://justinangel.net/SilverlightWeblogFeaturesOverview

-- Justin Angel

Posted by Justin-Josef Angel [MVP]
תגים:,

What ever happened to the Shabak blogs?

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

Service announcement in English: This is a tedious & boring blog post in Hebrew regarding something you don't care about.

תמונת מסך מאתר הבלוגים של השב"כ

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

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

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

אוהבים את השב"כ, דיאגרמה. כי אני אוהב דיאגרמות.

המערכת עלתה לאוויר במרץ 2008 וב-1 למאי 2008 פורסם בה הפוסט האחרון. סה"כ פחות מחודשיים של פעילות.

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

אולי אני אלך לעבוד בשב"כ?

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

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

 אכזבה - השב"כ איזה מזל שהם לא אחראים לדברים באמת חשובים

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

שב"כ, מביטים עלייך, ולוקחים נקודת נוכחות. עוד 3 כאלו ואנחנו מתקשרים לאמא ואבא.

 

-- ג'סטין-יוסף אנג'ל

(רוכש חברים חדשים)

Response.redirect או Server.Transfer?

תוספת משנת 2008: זהו. העברתי את כל הפוסטים מ-JustinAngel.Net משנת 2003-2005 לבלוג הזה ששירת אותי בין השנים 2006-2008. בעתיד הקרוב JustinAngel.Net ירד מהאוויר אחרי 6 שנים של להיות הבלוג הטכני הראשון שכתוב בעברית. הפוסט המצורף זה הפוסט הטכני הראשון שאי-פעם כתבתי. לא היה לנו אז צביעת קוד, ולא תמונות בבלוגים ושום כלי מתוחכם כזה. אני חושב שזה הולם שהפוסט הראשון שאי-פעם כתבתי בעברית יהיה גם הפוסט האחרון שאי-פעם אפרסם בעברית.
 
 
לא פעם נתקלתי עם מפתחים חדשים ומנוסים כאחד שלא בטוחים במספר סוגיות בתחום היותר אפור של הפן הטכני בתכנות דוט נט. פעם בשבוע אפרסם נושא אחד עם מאמרים הדנים בו.

והפעם: Response.redirect או Server.Transfer?
מאמר #1: סקירה כללית על הנושא
מאמר #2: פירוט על נושא ה-flow
מאמר #3: סיכום והרחבה


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

// Server.Transfer
// # First Overload:
Server.Transfer(string path)
Server.Transfer("myPage.Aspx")
// # Second Overload:
Server.Transfer(string path, bool PreserveForm)
Server.Transfer("myPage.Aspx",true)

// Response.Redirect
// # First Overload:
Response.redirect(string URL)
Response.redirect("myPage.aspx")
// # Second Overload:
Response.redirect(string URL, bool endResponse)
Response.redirect("myPage.aspx", false)

2. הבדלים בקונספט: Response.Redirect מיועד לשימוש לצד לקוח, Server.Transfer מיועד לשימוש בצד שרת. כל שאר ההבדלים נובעים מיכולותיו ומגבלותיו של כל צד.

3. שימור נתוני טופס: בעזרת Server.Transfer ניתן לשמר את המצב של הטופס הנוכחי לדף אליו מעבירים. הווה אומר, האוספים Request.Form ו-Request.QueryString. ניתן להשיג זאת בכך שמכוונים את המשתנה preserveForm ל-true. היתרון של הדבר שלמעשה כל העבודה שנעשתה עד כה בדף המפנה תהיה זמינה לשימוש בדף אשר אליו מפנים. החסרון של הדבר היא ששימור מצב הטופס דורש משאבי מערכת וכך הופך את התוכנית לפחות יעילה. חשוב לציין ש-viewState הרבה יותר מאובטח ומכיל את היכולת הזאת, אך גם יקר יותר בביצועים.

4. העברה בצד לקוח: כאשר אנו מבצעים Response.redirect מה שקורה למעשה הוא שהשרת שולח ללקוח הודעה שהדף עבר למקום אחר והדפדפן של הלקוח פונה לאותה הכתובת.

HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Location: somewhere/newlocation.aspx


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

5. העברה בצד שרת: כאשר אנו מבצעים Server.Transfer השרת למעשה מבצע עיבוד של webform אחר במקום זה שהלקוח ביקש. וכל זה, בלי שהלקוח יודע כלום. היתרון בכך שנחסך PostBack. החסרון הוא בכך שהדפדפן של הלקוח אינו מודע לכל התהליך הזה. הכתובת ב-address bar תישאר זהה לכתובת המבוקשת המקורית ולא לכתובת אשר אליה הפננו. באם ירצה הלקוח לשים סימניה על הדף הזה, הוא למעשה יסמן את הדף הקודם.

6. תכלס', חסכון: Server.Transfer מוריד את העומס על השרת והלקוח כאחד.

7. העברה הודעות: כאשר מבצעים העברה בעזרת Server.Transfer ניתן להעביר הערות\פרמטרים\הודעות בעזרת אסופת Content.

הערות? תוספות?

שבוע טוב,
ג'סטין-יוסף אנג'ל

SessionState בקובץ web.config

העשרה שבועית בנושא והפעם מאמר מקיף על האפשרויות של sessionState בתוך web.config. (נושא שנתתי הסבר מזורז עליו השבוע). (תודה לאוריאקס על הכותרת)

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

ספציפית במאמר זה נתמקד באפשרויות השונות לשימוש ב-sessionState כפי שהן באות לכדי ביטוי ב-web.config ו-machine.config:
1. כבוי. אם לא משתמשים ב-sessionState, זה חסכון אדיר במשאבי מערכת שמכוונים את ה-sessionState לכבוי. ככה המערכת לא צריכה להעסיק סתם משאבים ולדאוג לזה. תתפלאו, אבל יש הרבה אפליקציות בסיסיות ואפילו כאלו מתקדמות שאין להן שום צורך ב-sessionState.


<configuration>
   <system.web>
      <sessionState mode="off">
      </sessionState>
   </system.web>
</configuration>


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


<%@ Page EnableSessionState="false" %>


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

2. InProc - המצב הזה הוא המצב הראשון והבסיס ביותר של שימוש ב-sessionState. מה שלמעשה InProc אומר הוא שהכל התהליך של שמירת sessionState מתבצע In-process. הווה אומר, על הפרוסס\Appdomain של הASP.net.
החסרונות - אין לו את היכולות של האפשרויות הבאות. בנוסף, חסרון חשוב הוא שבmachine.config נמצאת תגית processModel אשר בין השאר מציינת מתי יש לאתחל את הפרוסס של ה-ASP.net (כאשר אין תגובה לתקופת זמן, כאשר יש עומס על השרת, כאשר אין תגובה ברשת וכך הלאה) וכאשר התהליך מאתחל את עצמו אז גם נמחק ה-sessionState.

כדי ליישם את מצב זה יש לשנות את mode ל-InProc בweb.config או machine.config:


<configuration>
   <system.web>
      <sessionState mode="InProc">
      </sessionState>
   </system.web>
</configuration>



3. StateServer - הקוסנפט מאוד פשוט: בואו נשמור את הנתונים על ה-sessionState ב-windows service.
יתרונות:
א. הפרוסס נפרד לחלוטין מהפרוסס של ה-ASP.net, ככה שניתן לאתחל את אפליקציית ה-IIS ולא לאבד את מידע ה-sessionState. הווה אומר, באתחול כפוי (ע"ע processModel) או באתחול ידני (מתוך inetmgr) ה-sessionState נשמר.
ב. העובדה שהנתונים במצב זה נשמרים ב-Windows service מאפשרים גם חיבור לשרת מרוחק. הווה אומר, שרת\מחשב אחד אשר שומר נתוני sessionState לכמה שרתים. הדבר הזה נהדר לחוות שרתים אשר מריצות אותה אפליקציית רשת.
דבר ראשון, אם מדובר באפליקציית רשת גדולה שדורשת יותר משרת אחד (למשל amazon.com) אז צריך שהמשתמש יוכל לעבוד על כל השרתים בלי להתחבר מחדש כל חמש שניות.
דבר שני, אם נופל שרת אפליקציה אחד ושרת אפליקציה אחר צריך לבוא ולמלות את המקום שלו אז לא נאבדים נתוני ה-sessionState.

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

חסרונות:
א. ככל שעולים יותר ויותר בפתרונות ל-sessionState מבחינת גמישות ואפשרויות ככה גם צריך יותר RAM\זכרון על השרת.
ב. כל דבר אשר שומרים ב-sessionState חייבת להיות אפשרות לעשות לו serialize. הווה אומר, כל אובייקט אשר לא ניתן לעשות לו serialize או שיזרוק שגיאה כאשר נכניס אותו ל-sessionState או פשוט לא ישמר.
ג. אין לאפשרות זאת כתיבה על הדיסק הקשיח. ככה שאתחול השרת אשר מפעיל את ה-state server עדיין ימחוק את כל נתוני ה-sessionState. אין שום דרך לגבות את הנתונים לדיסק ולטעון אותם מאוחר יותר.

איך ליישם:
א. Administrative Tools / Services  --> להפעיל את ה-ASP.NET State Service
ב. יש לשנות את הmode בתוך קובץ ה-web.config (או ה-machine.config) ל-"StateServer". כמו כן יש להוסיף את הנתונים הבאים על השרת אשר מריץ את ה-windows service: כתובת IP ודרך איזה פורט מתחברים. את שני הנתונים הללו נשים ב-stateConnectionString. בנוסף ניתן לקבוע משתנה stateNetworkTimeout, אשר קובע תוך כמה שניות מתרחש timeout בפרוטוקול ה-tcp/ip. חשוב לציין שאירוע ה-timeout נרשם בלוגים של השרת.


<configuration>
   <system.web>
      <sessionState
       mode="StateServer"
       stateConnectionString="tcpip=127.0.0.1:42424"
       stateNetworkTimeout=15>
      </sessionState>
   </system.web>
</configuration>


(בחיבור למעלה מבצע חיבור דרך Loopback. הווה אומר, השרת מתחבר לעצמו דרך 127.0.0.1. בנוסף באם אין תגובה מה-windows service תוך 15 שניות יתרחש timeout.)


4. SQLserver - נתוני ה-sessionState ישמרו בתוך מסד נתונים SQL server. למעשה מה שקורה הוא שיש ליצור מסד נתונים חדש בשם ASPstate, ובתוכו נתוני ה-sessionState נשמרים כנתונים מסוג BLOB (קובץ בינארי גדול).

יתרונות:
א. שרידות נתוני sessionState אדירה. אפשר לאתחל את ה-IIS, את תהליך ה-SQLserver, את שרת ה-SQL server עצמו, ושום דבר חוץ מלירות בהארד-דיסק לא יגרום לאיבוד נתונים.
ב. כמו ב-stateServer יש יתרון גדול בשימוש בחוות שרתים. רק צריך לזכור (זה תקף גם ל-stateServer) שבכדי שיהיה sessionState משותף לאותה אפליקציה על שני שרתים שמחוברים לאותו SQLserver/StateServer צריך אותו נתיב לאפליקציה.

חסרונות:
א. ככל שעולים יותר ויותר בפתרונות ל-sessionState מבחינת גמישות ואפשרויות ככה גם צריך יותר RAM\זכרון על השרת.
ב. כל דבר אשר שומרים ב-sessionState חייבת להיות אפשרות לעשות לו serialize. הווה אומר, כל אובייקט אשר לא ניתן לעשות לו serialize או שיזרוק שגיאה כאשר נכניס אותו ל-sessionState או פשוט לא ישמר.
ג. חובה שיהיה מסד נתונים SQL server. מדובר בחסרון אם האפליקציה עצמה עובדת עם מסד נתונים אחר, ולכן צריך לתחזק מסד נתונים נוסף.

כיצד מיישמים:
1. מריצים על שרת ה-SQLserver את InstallSqlState.sql.
2. אם עובדים ב-trusted connections עם שרת ה-SQLserver צריך לשנות את הבעלות על מסד ה-ASPstate.
3. אם עובדים עם SQL authentication, צריך ליצור משתמש שיהיה לו גישה למסד ASPstate (כולל הרצת פרוצדורות).
4. יש לשנות את mode בתוך קובץ ה-web.config (או machine.config) ל-"SQLServer". כמו כן יש לציין מחרוזת חיבור למסד (שם המסד, שם משתמש וסיסמא) במשתנה sqlConnectionString. בנוסף, כמו ב-stateServer ניתן לציין Timeout בנתון ה- stateNetworkTimeout.


<configuration>
   <system.web>
      <sessionState
       mode="SQLServer"
       sqlConnectionString="data source=server_name;user id=user_id;password=password"
       stateNetworkTimeout=15>
      </sessionState>
   </system.web>
</configuration>




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


בנוסף, קיימים שני משתנים נוספים בתוך תגית ה<sessionState>.
1. cookieless - מציין אם ה-sessionState עובד עם או בלי עוגיות בצד לקוח.


<configuration>
   <system.web>
      <sessionState cookieless="false">
      </sessionState>
   </system.web>
</configuration>



אם בחרנו לעבוד בלי עוגיות הנתיב היחסי של פנייה בין webforms ישתנה.
למשל:  


http://localhost/(lit3py55t21z5v55vlm25s55)/Application/SessionState.aspx

במקום:


http://localhost/Application/SessionState.aspx


ואת זאת מיישמים בכך שנכוון את משתנה ה-cookieless ל-true (קרי, לא להשתמש בעוגיות צד-לקוח).


<configuration>
   <system.web>
      <sessionState cookieless="true">
      </sessionState>
   </system.web>
</configuration>



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


<configuration>
   <system.web>
      <sessionState timeout="20">
      <!-- for 20 minutes -->
      <sessionState timeout="300">
     <!-- for 5 hours -->
      </sessionState>
   </system.web>
</configuration>




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

בביליוגרפיה עיקרית זמינה בקישורים.

שבוע טוב,
ג´סטין-יוסף אנג´ל



כתובות אינטרנט נילוות:
קישור #1: ASP.NET Session State ב-MSDN
קישור #2: ASP.NET Session State: Architectural and Performance Considerations
קישור #3: Using SQL Server for asp.net session state

TextBox.Readonly=true; האומנם?

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

תיכננתי ניסוי קטן שיבדוק את הנושא. ניצור דף עם הפריטים הבאים:
1. תיבת טקסט - פקד TextBox. התוכנה ReadOnly תהיה True, והתכונה Text תהיה על "Original Text".
2. כפתור - כפתור פשוט שיגרום ל-PostBack.
3.סקריפט ג'אווה - הסקריפט ייגש לתגית ה-<input> בצד לקוח וינסה לשנות את הערך שלה ל-"Not original Value!". הסקריפט ירוץ בטעינת הדף.
4. Label - לתוכה נדפיס את הערכים של ה-TextBox שלנו בכל טעינה של הדף.


1. נפתח טופס חדש. נשרטט את הכפתור ונקבל את ה-Design HTML הבא:


<asp:Button id="btnTesting" style="Z-INDEX: 102; LEFT: 520px; POSITION: absolute; TOP: 288px" runat="server" Width="184px" Height="56px" Text="Button"></asp:Button>


2. נשרטט את תיבת הטקסט ונקבע את התכונות אשר הזוכרו לעיל ונקבל את ה-Design HTML הבא:


   <asp:TextBox id="tbxTest" style="Z-INDEX: 101; LEFT: 536px; POSITION: absolute; TOP: 216px" runat="server" Width="136px" Height="24px" ReadOnly="True"> Original Text </asp:TextBox>

3. נוסיף פונקציה בג'אווה סקריפט שתחפש את תיבת הטקסט שלנו ותשנה את הערך שלה:


  <script type="text/javascript">
   function testing()
   {
    tbxTest = document.getElementById("tbxTest");
    tbxTest.value = "Not original Value!";
   }
  </script>

הקוד לעיל מאוד פשוט, כל מה שהוא עושה זה למצוא אלמנטים בקוד עם השם של תיבת הטקסט שלנו ומשנה את ערך תיבת הטקסט. כמו כן נרשום את האירוע לOnLoad בתגית ה-<body>:

<body MS_POSITIONING="GridLayout" onload="testing();">
 

4. נוסיף Label בצד שרת בשם lblTesting:


<asp:Label id="lblTesting" style="Z-INDEX: 103; LEFT: 488px; POSITION: absolute; TOP: 48px" runat="server" Height="128px" Width="264px"></asp:Label>

נוסיף באירוע ה-Page_load את הטקסט שיכניס את ערך תיבת הטקסט לתוך ה-Label:

  private void Page_Load(object sender, System.EventArgs e)
  {
   // Add TextBox value to Label
   lblTesting.Text += "TextBox Testing: " + tbxTest.Text + "<br>";
  }

כעת הטופס שלנו יראה בתצוגת Design כמו בתמונה הבאה:

image

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

אם ReadOnly=true בפקד TextBox נועל את הערך מצד השרת נצפה לראות את הערך המקורי של תיבת הטקסט, ללא קשר לאיזה שינויים בוצעו בצד לקוח.

לעומת זאת, אם נראה ב-Label לאחר ה-PostBack את הערך החדש שנתנו לו בפונקציית הג'אווה סקריפט הוכחנו ש-Readonly=true בפקד TextBox אינו שומר על הערך בצד שרת. למעשה אם כך המצב מה שיתבצע הוא שהשרת שולח את תיבת הטקס כמו כל תיבה רגילה וקורא בחזרה את ערכה כמו כל תיבה רגילה.

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

image

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

image

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

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

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

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

לסיכום ולחזרה בפעם שלישית, אין להשתמש ב-TextBox עם readonly=true במשהו כיותר מאפקט בממשק צד לקוח. הם אינם מאובטחים והמידע בהם נתון לשינויים בלתי רצויים. ואם כבר זה סה"כ אפקט בצד לקוח בכלל לא צריך לזה פקד. תחשבו לשנייה שאין שום סיבה שזה לא יהיה Literal control (פקד שאנחנו נכתוב לתוכו את ה-HTML שהוא יציג).

 

לקריאה נוספת: TextBox.ReadOnly Property, ב-MSDN

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

שלום לכולם,

נתחיל בהגדרה כוללנית, Late Bound Data Expressions מיועדים לקבלת מידע (משתנים) בתצוגת עיצוב ולפרמט אותו לתצורה נבחרת. נעשה פירוש רש“י. ב”קבלת מידע (משתנים)” אנו מתכוונים כל אובייקט המכיל מידע, הכל ממחרוזות, DataReaderים וכלה ב-DataSetים. ב”בתצוגת עיצוב” אנו מתכוונים שבזמן שתמיד ניתן לשנות את תוכן המידע כחלק מה-Code Behind, ה-Late Bound Data Expressions מאפשרים לנו לשנות את המידע בתצוגת ה-Design. ב”פרמט אותו לתצורה נבחרת” אנו אומרים למעשה שאנו נקבל את הנתונים האלו ונגדיר להם אלגוריתם שהם יעברו לפני תצוגה ללקוח.

ישנם שתי שיטות בנויות בדוט נט ל-Late Bound Data Expressions: הראשונה DataBinder.Eval, והשנייה String.Format. שתיהן למעשה מאפשרות לקחת מידע במהלך אירוע ה-OnDataBound ולהציגו או לשנות לו את אופי התצוגה. נסקור את שתי הפונקציות הללו במהלך אפשרויות א' ו-ב'. אם אתם כבר מכירים ומבינים את דרך פעולתם, אני ממליץ שתקפצו ישר לחלק ג'.


א.  DataBind my Page! (או, תראו את הפשטות)

בשיטה הזאת אנחנו עושים דבר מאוד הגיוני - DataBind לדף עצמו. נגדיר פקדים\משתנים ברמת הדף ונוכל לעשות להם DataBind. בואו נביט על דוגמה. החלטנו שאנו רוצים אפשרות לשנות בצורה תכנותית את הכותרת (<title>myTitle</title>) של הדף. כידוע, אין כזאת אפשרות בנויה כחלק מהפקד System.Web.UI.Page.

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

...
public class testingWebForm : System.Web.UI.Page
{
  // Declare Page Title string object
  protected string myStr;
 
  private void Page_Load(object sender, System.EventArgs e)
  {
    // Place some value in Page Title
   myStr = “פורום תפוז דוט נט“;
  /* פרסומת סמויה */
  }
}

כמו כל String אחר ברמת ה-Page הצהרנו עליו ונתנו לו ערך. עכשיו הגיע הזמן להתחיל לראות חלק מהקסם של Late Bound Data Expressions. בתצוגת Design HTML של הדף נשנה את הערך של <title> כך שהוא יהיה ביטוי שעושה DataBind.

<html>
  <head>
    <title><%# myStr %></title>
  </head>
...
</html>

ועכשיו נגיד שבאירוע Page_load שצריך לעשות Page.DataBind.

...
public class testingWebForm : System.Web.UI.Page
{
  // Declare Page Title string object
  protected string myStr;
 
  private void Page_Load(object sender, System.EventArgs e)
  {
    // Place some value in Page Title
   myStr = “פורום תפוז דוט נט“;
 
   // DataBind the page itself!
    this.DataBind();
  }
}

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

<head>
   <title>פורום תפוז דוט נט</title>
</head>

עד כה הכל היה מאוד פשוט: עשינו DataBind לפקד ברמת הדף לתוך תצוגת ה-Design שלנו. בואו ניקח את זה צעד אחד קדימה וננסה לעשות DataBind לאובייקט יותר רציני מאשר String, למשל DropDownList. אנו רוצים שהטקסט של הפריט הנבחר ב-DropDownList יוצג כחלק מהדף לאחר PostBack.

  // WebForm.aspx
   <!-- This DropDownList will contain two ListItems -->
   <asp:DropDownList Runat="server" ID="myDDL">
       <asp:ListItem>לאוטובוס עלתה גברת עם סלים</asp:ListItem>
       <asp:ListItem>האוטובוס נסע לפני שהגברת עם הסלים עלתה</asp:ListItem>
   </asp:DropDownList>

   <!-- This Button Will case a PostBack and Call myBtn_Click -->
   <asp:Button Runat="server" Text="Cause PostBack" OnClick="myBtn_Click" id="Button1" />
 
   <!-- This Late Data Bound expression will diplay what is the currently selected item
   <%# myDDL.SelectedItem.Text %>
// WebForm.asp.cs
  public void myBtn_Click(object sender, System.EventArgs e)
  {
    // On button click - DataBind the Page
     this.DataBind();
  }

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

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


ב. DataBinder.Eval (או: כמו בלונדינית - טיפש וחמוד)

כאן מתגלה חלק מהיופי של Late Bound Data Expressions. השיטה DataBinder.Eval נמצאת בשימוש רב בפקדים DataGrid, DataList ו-Reapter. בפקדים אלו יש לנו אפשרות לכתוב כמעט את כל הקוד שמעצב את המידע בתצוגת Design. בואו נביט על מה למעשה קורה למשל ב-DataBinding מ-DataView: כאשר ביקשנו כזו DataBinding, כל שורה נשלחת כ-Container.DataItem ומבצעת DataBinding לקוד שמעצב את המידע.

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

שם הגברת עם סלים

כמות הסלים

מחיר הסלים

strOldLadyWithBags_name
intNumberOfBags

intFinalCost

לאה

2

10

אסתר

8

2,000

סימה

4

50

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

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
   לגברת עם הסלים קוראים
   <%# DataBinder.Eval(Container.DataItem, "strOldLadyWithBags_name")%>
  </ItemTemplate>
</asp:DataList>

נניח ועשינו שאילתא שמחזירה שמות של של נשים זקנות עם סלים ול-DataView שלה עשינו DataBind ל-myDataList. כמו כן נניח שהרשימה מחזירה: לאה, אסתר וסימה. אם נריץ את הדף נקבל: “לגברת עם הסלים קוראים לאה. לגברת עם הסלים קוראים אסתר. לגברת עם הסלים קוראים סימה“.

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

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# DataBinder.Eval(Container.DataItem, "strOldLadyWithBags_name")%> 
     עלתה לאוטובוס עם
     <%# DataBinder.Eval(Container.DataItem, "intNumberOfBags")%> 
   סלים
   שעלו סה“כ
   <%# DataBinder.Eval(Container.DataItem, "intFinalCost")%> 
  </ItemTemplate>
</asp:DataList>

אם נריץ הדוגמה נקבל “לאה עלתה לאוטובוס עם 4 סלים שעלו סה“כ 100. אסתר עלתה לאוטובוס עם 8 סלים שעלו סה“כ 2,000. סימה עלתה לאוטובוס עם 2 סלים שעלו סה”כ 50.”. עד עכשיו, הראנו כיצד בתוך ItemTemplete של DataList, DataGrid ו-Repeater ניתן להציג מידע בצורה חוזרת מתוך מסד הנתונים.

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

השיטה DataBinder.Eval מקבלת שלושה פרמטרים: הפקד שמכיל את המידע (DataContainer), לאיזה שדה\תא אנו מתייחסים בפקד שמכיל את המידע והשלישי שהוא אופציונאלי זה כיצד לפרמט את המחרוזת. בואו נראה דוגמה כיצד נפרמט את מחיר הסלים כך שיהיה בתצורת מטבע.

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# DataBinder.Eval(Container.DataItem, "strOldLadyWithBags_name")%>
     עלתה לאוטובוס עם
     <%# DataBinder.Eval(Container.DataItem, "intNumberOfBags")%>
   סלים
   שעלו סה“כ
   <%# DataBinder.Eval(Container.DataItem, "intFinalCost", {0:c})%> 
  </ItemTemplate>
</asp:DataList>

וכאשר נציג את המסך נראה:  “לאה עלתה לאוטובוס עם 4 סלים שעלו סה“כ 100₪. אסתר עלתה לאוטובוס עם 8 סלים שעלו סה“כ 2,000₪. סימה עלתה לאוטובוס עם 2 סלים שעלו סה”כ 50₪.”

נכון שזה נראה הרבה יותר טוב? ישנן אפשרויות רבות רבות להכיל פורמט תצוגה מסויים על מידע. אפשר לפרמט תאריך להיות תאריך ארוך, קצר, לפי שעון בינלאומי, לועזי קצר, אירופאי קצר. אפשר לפרמט שעות להכיל רק שעות, רק שעות ודקות, להציג AM/PM. אפשר לפרמט להציג מספר כמטבע, כאחוזים, כמספר מדעי, לאכוף מספר ספרות אחרי הנקודה. והאפשרויות רבות. כנראה שהחבר'ה במיקרוסופט באמת חשבו על לפרמט נתונים שונים לפורמטים מסויימים כשהם יצרו את האפשרות הזאת. בהמשך נראה את הכוח האמיתי של Late Bound Data Expressions.


ב. String.Format (או: יותר מרחב פעולה, הרבה יותר פעולות סיזיפיות)

DataBinder.Eval עושה בשבילנו שתי פעולות: הראשונה, חוסכת מאתנו פעולות סיזפיות של המרה לפני שנוכל לגשת למידע. השנייה, אם ביקשנו לפרמט את המידע היא קוראת בשבילנו ל-String.Format. מבחינת נקודת המבט שלנו מדובר על מתודות שקולות ועד סוף האפשרות הזו נדגים זאת.

מה זאת אומרת ש-DataBinder.Eval חוסכת מאתנו פעולות סיזפיות של המרה? בואו נביט על כיצד הדוגמה למעלה הייתה נראית אם היינו עושים אותה ב-String.Format:

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# String.Format("{0}", ((DataRowView)Container.DataItem) ["strOldLadyWithBags_name"]) %> 
     עלתה לאוטובוס עם
     <%# String.Format("{0}", ((DataRowView)Container.DataItem) ["intNumberOfBags"]) %>
   סלים
   שעלו סה“כ
     <%# String.Format("{0:c}", ((DataRowView)Container.DataItem) ["intFinalCost"]) %>
  </ItemTemplate>
</asp:DataList>

פשוט מאוד לראות שעכשיו בכל DataBind צריך לעשות גם המרה (Casting) של Contrainer.DataItem לסוג DataRowView ולגשת לתא מסויים בתוכו לפי שם העמודה. כמו כן שימו לב ש-intFinalCost (עלות הסלים) עדיין בפורמט של מטבע.

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

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# String.Format(“{0} עלתה לאוטובוס עם {1} סלים במחיר {2:c}“,
       ((DataRowView)Container.DataItem) ["strOldLadyWithBags_name"]),
       ((DataRowView)Container.DataItem) ["intNumberOfBags"]),
       ((DataRowView)Container.DataItem) ["intFinalCost"])   %>
  </ItemTemplate>
</asp:DataList>

המשפט שכתבנו, {0} עלתה לאוטובוס עם {1} סלים במחיר {c:2}, דומה מאוד למשפט מוכר של Console.WriteLine. למעשה הוא מציב את הערכים שבאים אחריו לתוכו בתוך משפט. שתי פעולות מתבצעות: פירמוט לתוך המשפט, פירמוט אישי לכל נתון (למשל, הפירמוט של המחיר למטבע). הדוגמה הזאת שקולה לחלוטין מבחינת תוצאות לדוגמה שלפניה.


ג. String.Format ופונקציות (או: כמה פשוט לשכלל מקרי קצה)

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

שם הגברת עם סלים

כמות הסלים

מחיר הסלים

strOldLadyWithBags_name
intNumberOfBags

intFinalCost

לאה

2

10

null

8

2,000

סימה

4

100

שימו לב לשינוי: אסתר שכחה את תג השם שלה ב-Dot net on the beach. אם ננסה להריץ את הדוגמה הקודמת (מועתקת למטה) נקבל: “לאה עלתה לאוטובוס עם 2 סלים במחיר 10₪. עלתה לאוטובוס עם 8 סלים במחיר 2,000₪. סימה עלתה לאוטובס עם 4 סלים במחיר 100₪.“.

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# String.Format(“{0} עלתה לאוטובוס עם {1} סלים במחיר {2:c}“,
       ((DataRowView)Container.DataItem) ["strOldLadyWithBags_name"]),
       ((DataRowView)Container.DataItem) ["intNumberOfBags"]),
       ((DataRowView)Container.DataItem) ["intFinalCost"])   %>
  </ItemTemplate>
</asp:DataList>

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

namespace myProject
{
  public class myPage : System.Web.UI.Page
  {
    ...
   public string FormatGivratName(object GivartName)
   {
     // Check if Givart Im Salim has name
     if (GivartName == null)
     {  // If Givart Im Salim has no name
        return “גברת ללא שם“;
     }
     else
     { // Givart Im Salim has name
         return GivartName.ToString();
     }
   }
  }
}

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

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# String.Format(“{0} עלתה לאוטובוס עם {1} סלים במחיר {2:c}“,
       FormatGivratName(((DataRowView)Container.DataItem) ["strOldLadyWithBags_name"])),
       ((DataRowView)Container.DataItem) ["intNumberOfBags"]),
       ((DataRowView)Container.DataItem) ["intFinalCost"])   %>
  </ItemTemplate>
</asp:DataList>

כאשר נריץ את הדוגמה הזאת נקבל: “לאה עלתה לאוטובוס עם 2 סלים במחיר 10₪. גברת ללא שם עלתה לאוטובוס עם 8 סלים במחיר 2,000₪. סימה עלתה לאוטובס עם 4 סלים במחיר 100₪.“

כמו שפירמטנו נתון אחד לפי תנאי מאוד קל, אפשר גם לפרמט כל נתון אחר לפי כל תנאי שנבחר. שימוש נפוץ בטכניקה הזאת היא כאשר אנו מחזירים ממסד הנתונים סימול שהפירוש שלו לא במסד הנתונים, אפשר להשתמש בכזו פונקציה בכדי להחליף את הסימול בטקסט המתאים ובכך ליישם Late Bound Data Expressions. למעשה הכוח של הפונקציות האלו שהוא נותן לנו את האפשרות לפרמט איך שנרצה את הנתונים שאנו מקבלים וכל זאת מתוך תצוגת Design של האפליקציה שלנו.


ד. רק פונקציות (או: When the *** hits the fan we get gold)

נשנה עוד את הטבלה שלנו:

שם הגברת עם סלים

כמות הסלים

מחיר הסלים

strOldLadyWithBags_name
intNumberOfBags

intFinalCost

לאה

0

0

null

8

2,000

סימה

4

0

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

אם נריץ את הדוגמה לעיל עם הנתונים האלו נקבל: “לאה עלתה לאוטובוס עם 0 סלים במחיר 0₪.גברת ללא שם עלתה לאוטובוס עם 8 סלים במחיר 2,000₪. סימה עלתה לאוטובס עם 4 סלים במחיר 0₪.“

למרות שהכל מודפס ויחסית ברור, הנתונים עצמם נראים מגוחך. מה זה 0 סלים של לאה? ללאה אין סלים!  מה זה מחיר 0 ש”ח? זה חינם! ככה אנחנו רוצים שיראה בסוף המשפט שלנו: “לאה עלתה לאוטובוס בלי סלים. גברת ללא שם עלתה לאוטובוס עם 8 סלים במחיר 2,000₪. סימה עלתה לאוטובס עם 4 סלים בחינם.“

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

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

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

using System.Text;
...
namespace myProject
{
  public class myPage : System.Web.UI.Page
  {
    ...
   public string FormatGivrat(object strGivartName, object intSalimAmount, object intPrice)
   {
     // Use a stringBuilder
     StringBuilder mySB = new StringBuilder();
  
     // Add GivartName to mySB
     mySB.Append(FormatGivratName(strGivartName));
     mySB.Append(“ עלתה לאוטובוס“);
 
     // Check if Givart Has salim
     if ((intSalimAmount == null) || (intSalimAmount.ToString() == “0“))
     { // Givart Doesn't have Salim
        mySB.Append(“ ללא סלים.“);
     }
     else
     { // Givart Has Salim
        mySB.Append(“ עם “ +  intSalimAmount.ToString() + “ סלים“);
       
       // Check If there is a Price
        if ((intPrice == null) || (intPrice.ToString() == “0“))
        { // No price
          mySB.Append(” בחינם.”);
        }
        else
        { // there is a price
          mySB.Append(“ במחיר “);
          // Add intPrice and format it to Currency
          mySB.Append( String.Format( “{0:c}“, intPrice.ToString() ) );
        }
      }
    
    // Return result from StringBuilder
    return mySB.ToString();
   }
 
   public string FormatGivratName(object GivartName)
   {
     // Check if Givart Im Salim has name
     if (GivartName == null)
     {  // If Givart Im Salim has no name
        return “גברת ללא שם“;
     }
     else
     { // Givart Im Salim has name
         return GivartName.ToString();
     }
   }
  }
}

נקרא לפונקציה מתוך הקוד בתצוגת Design:

<asp:DataList id="myDataList" runat="server">
  <ItemTemplate>
     <%# FormatGivrat(
       ((DataRowView)Container.DataItem) ["strOldLadyWithBags_name"]),
       ((DataRowView)Container.DataItem) ["intNumberOfBags"]),
       ((DataRowView)Container.DataItem) ["intFinalCost"]) )   %>
  </ItemTemplate>
</asp:DataList>

והתוצאה עכשיו אכן תהיה: “לאה עלתה לאוטובוס בלי סלים. עלתה לאוטובוס עם 8 סלים במחיר 2,000₪. סימה עלתה לאוטובס עם 4 סלים בחינם.“

הכוח שקיבלנו עכשיו זה להפסיק את התלות הנמשכת בפונקציות כמו String.Format ו-DataBinder.Eval ואם אנו צריכים ליישם חוקיות משלנו מסיבות של תחביר או תצוגה או כל דבר, נוכל לעשות זאת מתוך תצוגת Design.


אחרית דבר

במאמר זה סקרנו את מגוון האפשרויות הקיימות לפירמוט מחרוזות מתוך תצוגת Design. התחלנו מלעבוד על הפונקציה DataBinder.Eval הבסיסית שכולם מכירים. עברנו ל-String.Format שמאוד דומה (ולמעשה גם היה אפשר לעשות עם DataBinder.Eval את כל מה שעשינו עם String.Format).  אחר כך הראנו מצב שבו עיצוב סטטי רגיל והצבת ערכים פשוט לא מתאימים. אצלנו זה היה מהסיבה של תחביר עברי, אבל יש עוד מגוון סיבות שהעתקה של הנתונים אחד-על-אחד לא מתאימים וצריך לשלב חוקיות משלנו. אחר כך הראנו כיצד לעבוד אם כל הביטוי עצמו בתוך <itemTemplete> צריך חוקיות משלו.

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

 

קונפיגיורציה לאפליקציות בקובץ ה-web.config, מדוע וכיצד?

קדם דבר:

שלום לכולם,

במאמר נערוך סקירה על מגוון האפשרויות לשמירה של קונפיגיורציה לאפליקציות (Application Configuration) בתוך קובץ ה-web.config.

שתי השאלות הראשונות שנשאל הן: מהי קונפיגיורציה אפלקטיבית? ומדוע לשמור ב-web.config?

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

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

למה לשמור את מחרוזת החיבור בקובץ XML חיצוני למשל ולכתוב אותה בגוף הקוד (Hard-coded)? מחר עוברים שרת מסד נתונים, אז מה תעשו? תעברו קובץ קובץ באפליקציה ותשנו את מחרוזת החיבור? זה רק הגיוני לשמור את הנתון הזה במקום אחד כדי להקל על פריסת האפליקציה ועל התחזוקה שלה. סיכמנו שנשמור את מחרוזת החיבור בקובץ XML.

שני הנתונים לדוגמה שהעלנו נשמור בקבצי XML. אבל יש ביניהם הבדל מהותי: אחד הוא מידע\נתון אפלקטיבי (Application Data)  והשני הוא קונפיגיורציה אפלקטיבית (Application Configuration). מידע אפלקטיבי זה נתונים שדרושים לאפליקציה מבחינת הנתונים אשר היא מכילה. לעומת זאת, קונפיגיורציה אפלקטיבית אלו נתונים אשר נדרשים לאפליקציה לפעול מבחינה שבעלדיהם לא תוכל לרוץ. אלו הם הנתונים שנדרשים להפעיל את האפליקציה.

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

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

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

 

אופציה א':  appSettings (או “הדרך הקלה, הטיפשה והפשוטה“)

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

key/value pairs (בעברית: זוגות מפתח-ערך): הרעיון והביצוע מאוד פשוטים, ניצור תגיות שיש להן שתי XML attributes. התכונה ראשונה, key, שתשמש אותנו כמפתח ראשי לחפש ערך ספציפי. התכונה השנייה, Value, תקבע ערך לאותו מפתח. ככה למשל יכול להיות לי מפתח בשם “DataBaseConnectionString“ אשר ערכו הוא מחרוזת החיבור למסד הנתונים.

כל זוג מפתח-ערך חייב לשבת בתוך תגית XML ראשית אשר מכילה אותו וזוגות מפתחות-ערכים נוספים. ביחס לאותה תגית XML ראשית אפשר לבצע שלוש פעולות: add, remove, clear. כשמו כן הוא add מוסיף מפתח עם ערך לתגית ה-XML. פעולת ה-remove מורידה מפתח מתגית ה-XML. ופעולת clear מורידה את כל המפתחות מתגית ה-XML.

במקום להגיד “תגית XML ראשית“ נדבר על אוסף אמיתי - אוסף appSettings. לאוסף זה ניתן להוסיף, להוריד ולנקות ערכים. אוסף זה הוא תגית אפשרית בתוך קובץ ה-web.config. בואו נביט על דוגמה:

<configuration>
  <appSettings>
    <add key="DataBaseConnectionString" value="data source=ServerName; initial catalog= Northwind;" />
  </appSettings>
</configuration>

בקוד למעלה הוספנו מפתח DataBaseConnectionString עם הערך: “data source=ServerName; initial catalog= Northwind;“. אם נחליט בעוד שעה להתחבר למסד נתונים ערך, כל מה שנצטרך זה לשנות את הערך בתוך web.config. אם מחר עוברים מבנייה בסביבת פיתוח לסביבת ריצה כל מה שנצטרך לעשות זה לשנות את שם השרת בתוך web.config.

כדי לראות את המפתח-ערך שהוספנו צריך להשתמש במרחב השמות System.Configuration.

using System.Configuration;
...
private void Page_Load(object sender, System.EventArgs e)
{
  // We can print out the value from the web.config:
  Response.Write(System.Configuration.ConfigurationSettings.AppSettings["DataBaseConnectionString"]);
  // shorter way:
  Response.Write(ConfigurationSettings.AppSettings["DataBaseConnectionString"]);
  // Or we can set the ConnectionString of our Connection to it:
  SqlConnection conSql = new SqlConnection();
  conSql.ConnectionString = ConfigurationSettings.AppSettings["DataBaseConnectionString"];
}

כמובן שניתן לעשות עוד המון דברים עם appSettings. אפשר להוסיף עוד מפתחות-ערכים וליישם אותם במקומות שונים באפליקציה.  כמו בכל אוסף מפתחות-ערכים גם אפשר להוריד (remove) או לנקות (clear) ערכים מתוך האוסף.


אופציה ב':  Custom Web.config tags, configSections (או “מאמא מיה Handlerים!“)

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

איך נכתוב את זה ב-appSetting? אנחנו לא. במקום זה ניצור תגית XML חדשה (Custom XML tags) לכל תגית. אבל במקום להגיד תגית XML חדשה נגיד - Section. נבנה שלושה Section-ים חדשים כ-XML רגיל.


<Riza> <!-- קונפיגיורציה לריצה -->
  <add key="BackGroundColor" value="blue" />
  <add key="FontColor" value="white" />
</Riza>
<Pitoch> <!-- קונפיגיורציה לפיתוח -->
  <add key="BackGroundColor" value="white" />
  <add key="FontColor" value="black" />
</Pitoch>
<Testing> <!-- קונפיגיורציה לבדיקות -->
  <add key="BackGroundColor" value="red" />
  <add key="FontColor" value="yellow" />
</Testing>

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

עכשיו כל זה היה קצת פשוט מדי, נכון? אז הגיע הזמן לסבך. אי-אפשר סתם ליצור Sectionים חדשים. לא כותבים Sectionים איך שבא לנו ומקווים שה-web.config ידע לקרוא אותם. לא, צריך להגיד ל-web.config מי בדיוק יטפל בהם. הגדרנו Sectionים חדשים וכעת נקבע מי יטפל בכל אחד מהם. בשביל זה נוסיף את תגית ה-<configSections>. בתוך אותה תגית נצהיר על ה-Sectionים החדשים ונקבע מי יטפל בהם.


<configSections>
   <section name="Riza" type="System.Configuration.NameValueSectionHandler" />
   <section name="Pitoch" type="System.Configuration.NameValueSectionHandler" />
   <section name="Testing" type="System.Configuration.NameValueSectionHandler" />
</configSections>

הצהרנו על שלושה Section-ים חדשים ואמרנו שמי שיטפל בהם הוא System.Configuration.NameValueSectionHandler. למעשה מה שאמרנו זה “שאנחנו ניגש באפליקציה שלנו ונוציא את ה-Section הזה תחזיר לנו בבקשה NameValueCollection”. אנחנו נדבר בהמשך על לקבל סוגים שונים של פקדים. סה”כ קובץ ה-web.config שלנו נראה ככה:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   
<configSections>
  <section name="Riza" type="System.Configuration.NameValueSectionHandler" />
  <section name="Pitoch" type="System.Configuration.NameValueSectionHandler" />
  <section name="Testing" type="System.Configuration.NameValueSectionHandler" />
</configSections>
   
<Riza> <!-- קונפיגיורציה לריצה -->
  <add key="BackGroundColor" value="blue" />
  <add key="FontColor" value="white" />
</Riza>
<Pitoch> <!-- קונפיגיורציה לפיתוח -->
  <add key="BackGroundColor" value="white" />
  <add key="FontColor" value="black" />
</Pitoch>
<Testing> <!-- קונפיגיורציה לבדיקות -->
  <add key="BackGroundColor" value="red" />
  <add key="FontColor" value="yellow" />
</Testing>
  <system.web>
  ... 
</system.web>
</configuration>

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


using System.Configuration;
using System.Collections;
using System.Collections.Specialized;
...
private void Page_Load(object sender, System.EventArgs e)
  {
  // We Retrive our “Pitoch“ Section
   System.Collections.Specialized.NameValueCollection myNVC = (System.Collections.Specialized.NameValueCollection)ConfigurationSettings.GetConfig("Pitoch");
 
  // We will print out the Background Color & Font Color
   Response.Write(myNVC.GetValues("BackGroundColor")[0].ToString() +
                                 "<br>" +
                                 myNVC.GetValues("FontColor")[0].ToString() +
                                 "<br>");

  }

נעבור על הדוגמה. דבר ראשון אמרנו אילו NameSpaced צריך כדי שכל הפקדים שלנו יפעלו. ספציפית צריך את System.Collections.Specialized בשביל NameValueCollection. אחר עברנו על ה-NameValueCollection והדפסנו את BackGroundColor ואת- FontColor. ואכן, יודפיס white ו-black.

בקובץ ה-web.config שלנו קבענו כי ה-Sectionים שלנו יחזירו NameValueCollection. נשנה את זה ונגיד שאנו רוצים ש-Riza יחזיר Dictionary.


<configSections>
  <section name="Riza" type="System.Configuration.DictionarySectionHandler" />
  <section name="Pitoch" type="System.Configuration.NameValueSectionHandler" />
  <section name="Testing" type="System.Configuration.NameValueSectionHandler" />
</configSections>

וברמת האפליקציה ניגש לנתונים של Riza באופן הבא:


  
using System.Configuration;
using System.Collections;
...
private void Page_Load(object sender, System.EventArgs e)
  {
   // We Retrive our “Riza“ Section
   System.Collections.IDictionary myIDC = (System.Collections.IDictionary) ConfigurationSettings.GetConfig("Riza");
 
   // We will print out the Background Color & Font Color
   Response.Write(myIDC["BackGroundColor"].ToString() +   
                                "<Br>" +
                                myIDC["FontColor"].ToString());

}

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

בואו נשים כאן סיכום של מה שלמדנו עד כה:


/* Web.config */
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   
    <configSections>
        <section name="Riza" type="System.Configuration.DictionarySectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <section name="Pitoch" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <section name="Testing" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </configSections>
   
     
<Riza> <!-- קונפיגיורציה לריצה -->
  <add key="BackGroundColor" value="blue" />
  <add key="FontColor" value="white" />
</Riza>
<Pitoch> <!-- קונפיגיורציה לפיתוח -->
  <add key="BackGroundColor" value="white" />
  <add key="FontColor" value="black" />
</Pitoch>
<Testing> <!-- קונפיגיורציה לבדיקות -->
  <add key="BackGroundColor" value="red" />
  <add key="FontColor" value="yellow" />
</Testing>
  <system.web>
  ....
</system.web>
</configuration>
 
 
/* WebForm */
using System.Configuration;
using System.Collections;
using System.Collections.Specialized;
...
private void Page_Load(object sender, System.EventArgs e)
  {
  // We Retrive our “Pitoch“ Section
   System.Collections.Specialized.NameValueCollection myNVC = (System.Collections.Specialized.NameValueCollection)ConfigurationSettings.GetConfig("Pitoch");
 
  // We will print out the Background Color & Font Color
   Response.Write(myNVC.GetValues("BackGroundColor")[0].ToString() +
                                 "<br>" +
                                 myNVC.GetValues("FontColor")[0].ToString() +
                                 "<br>");
 
   // We Retrive our “Riza“ Section
   System.Collections.IDictionary myIDC = (System.Collections.IDictionary) ConfigurationSettings.GetConfig("Riza");
 
   // We will print out the Background Color & Font Color
   Response.Write(myIDC["BackGroundColor"].ToString() +   
                                "<Br>" +
                                myIDC["FontColor"].ToString());

  }

 

אופציה ג':  sectionGroup (או “ועכשיו כולם ביחד!“)

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


<SvivotPitoch> <!-- קבוצת קונפיגיורציות בשם סביבות פיתוח -->
  <Riza> <!-- קונפיגיורציה לריצה -->
  <add key="BackGroundColor" value="blue" />
    <add key="FontColor" value="white" />
  </Riza>
  <Pitoch> <!-- קונפיגיורציה לפיתוח -->
    <add key="BackGroundColor" value="white" />
    <add key="FontColor" value="black" />
  </Pitoch>
  <Testing> <!-- קונפיגיורציה לבדיקות -->
    <add key="BackGroundColor" value="red" />
    <add key="FontColor" value="yellow" />
  </Testing>
</SvivotPitoch>

  נכון שזה נראה הרבה יותר מסודר? אולי במצב הזה של שלושה sectionים זה לא נראה הרבה, אבל תחשבו שאם היינו יוצרים למשל Section לכל שרת שהמסד יכול להתחבר אליו. בארגון בינוני+ מדובר על 20+ שרתים, זה המון Sectionים סתם לזרוק בתוך ה-web.config.

אנחנו כבר למודי web.config ויודעים שאי-אפשר סתם ככה להוסיף Sectionים. באותו אופן גם אי-אפשר להוסיף SectionGroup בלי להצהיר אליה ב-<configSections>.


     
<configSections>
  <sectionGroup name="SvivotPitoch">
    <section name="Riza" type="System.Configuration.DictionarySectionHandler" />
    <section name="Pitoch" type="System.Configuration.NameValueSectionHandler" />
    <section name="Testing" type="System.Configuration.NameValueSectionHandler" /> 
  </sectionGroup>
</configSections>
  
 

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


// Previously:
IDictionary myIDC = (IDictionary) ConfigurationSettings.GetConfig("Riza");
 
// Now:
IDictionary myIDC = (IDictionary) ConfigurationSettings.GetConfig("SvivotPitoch/Riza");

שימו לב לשינוי, אנחנו למעשה עובדים עם Section בתוך sectionGroup.

נסכם את המצב עד כה:


/* Web.config */
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   
<configSections>
  <sectionGroup name="SvivotPitoch">
    <section name="Riza" type="System.Configuration.DictionarySectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="Pitoch" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <section name="Testing" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> 
  </sectionGroup>
</configSections>
   
<SvivotPitoch> <!-- קבוצת קונפיגיורציות בשם סביבות פיתוח -->
  <Riza> <!-- קונפיגיורציה לריצה -->
  <add key="BackGroundColor" value="blue" />
    <add key="FontColor" value="white" />
  </Riza>
  <Pitoch> <!-- קונפיגיורציה לפיתוח -->
    <add key="BackGroundColor" value="white" />
    <add key="FontColor" value="black" />
  </Pitoch>
  <Testing> <!-- קונפיגיורציה לבדיקות -->
    <add key="BackGroundColor" value="red" />
    <add key="FontColor" value="yellow" />
  </Testing>
</SvivotPitoch>
  <system.web>
  ...
</system.web>
</configuration>
 
 
/* WebForm.aspx */
using System.Configuration;
using System.Collections.Specialized;
...
private void Page_Load(object sender, System.EventArgs e)
  {
   // We Retrive our “Pitoch“ Section
   System.Collections.Specialized.NameValueCollection myNVC = (System.Collections.Specialized.NameValueCollection) ConfigurationSettings.GetConfig("SvivotPitoch/Pitoch");
 
   // We will print out the Background Color & Font Color
   Response.Write(myNVC.GetValues("BackGroundColor")[0].ToString() +
                                                               "<br>" +
                                                                myNVC.GetValues("FontColor")[0].ToString() +
                                                               "<br>");
 
   // We Retrive our “Riza“ Section
   System.Collections.IDictionary myIDC = (System.Collections.IDictionary) ConfigurationSettings.GetConfig("SvivotPitoch/Riza");
 
   // We will print out the Background Color & Font Color
   Response.Write(myIDC["BackGroundColor"].ToString() +
                                                               "<Br>" +
                                                               myIDC["FontColor"].ToString());
  }

(השינויים מודגשים)

 

אופציה ד':  XmlNode, CustomHandlers (או “סוף סוף נפתרנו מהמפתחות-ערכים האלו!“)

הגענו דרך ארוכה עד כאן... פיתחנו בתוך ה-web.config שלנו תגיות שנראות ממש כמו XML והכל במינימום מאמץ. אבל יש עוד שני דברים שמציקים לי:

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

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

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


public class XmlNodeSectionHandler : IConfigurationSectionHandler
{
    public object Create(object parent, object configContext, XmlNode section)
    {
      return section;
    }
}

אני יודע שעכשיו חלקכם תוהים קצת מה בדיוק כתוב למעלה. אתם זוכרים שהגדרנו configSections? כשהגדרנו <section> תמיד גם אמרנו type. התכונה הזאת, type, מתייחסת לאיזה פקד מסוג sectionHandler יטפל בבקשה שלנו ל-ConfigurationSettings.GetConfig. בקוד למעלה כתבתי sectionHandler שממש את הממשק הנדרש ל-SectionHandlerים ומחזיר XmlNode לפי הנתיב שנבקש. קצת מסובך, אני יודע, אבל זה הכי פשוט שיש.

בואו נביט על קובץ ה-web.config שלנו:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   
    <configSections>
    <section name="SvivotPitoch" type="ProjectNameSpace.XmlNodeSectionHandler, ProjectNameSpace" />
  </configSections>
   
<SvivotPitoch>
  <Riza>
    <BackGroundColor>blue</BackGroundColor>
    <FontColor>white</FontColor>
  </Riza>
  <Pitoch>
     <BackGroundColor>white</BackGroundColor>
    <FontColor>black</FontColor>
  </Pitoch>
  <Testing>
    <BackGroundColor>red</BackGroundColor>
    <FontColor>yellow</FontColor>
  </Testing>
</SvivotPitoch>
  <system.web>
  ...
</system.web>
</configuration>

תראו איזה יופי, כל כך נקי, כל כך מסודר. סה“כ הצהרנו על Section אחדבלבד וכתבנו אותו ב-XML רגיל לחלוטין. תשימו לב שבהצהרה על ה-Section כתבנו הפנייה לProjectNameSpace.XmlNodeSectionHandler שזו הפנייה ל-SectionHandler שלנו.

עכשיו נביט על הקוד שלנו:


using System.Configuration;
using System.Xml;
...
namespace ProjectNameSpace
{
  public class WebForm1 : System.Web.UI.Page
  {
    private void Page_Load(object sender, System.EventArgs e)
    {
       // Retrive the XMLnode of SvivotPitoch into myXML
       XmlNode myXML = (XmlNode) ConfigurationSettings.GetConfig("SvivotPitoch");
       // Print the Testing/BackGroundColor  
       Response.Write(myXML.SelectNodes("Testing/BackGroundColor")[0].InnerText);
    }
    ...
  }
 
  public class XmlNodeSectionHandler : IConfigurationSectionHandler
  {
    public object Create(object parent, object configContext, XmlNode section)
    {
      return section;
    }
  }
}

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

 

אחרית דבר

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

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

תרגיל בארכיקטורה - תיאוריה בחסות האקדח והמציאות שלי

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

שלום לכולם,

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


איך הייתם ניגשים לממש שאלה כזו מבחינת האפיון וגם תכנותית (.NET)?

1. שתי ישויות - CONTACT , CONTACT-GROUP  (לטובת EMAIL)

2. CONTACT-GROUP יכולה להכיל גם CONTACT וגם CONTACT-GROUP

3. CONTACT יכול להיות במספר CONTACT-GROUPS או באף אחת

4. יש לאפין ולכתוב תכנה המאפשרת
   א. יצירת CONTACT  ו CONTACT-GROUP
   ב. הוספת CONTACT ל CONTACT-GROUP
   ג. מחיקת CONTACT מ CONTACT-GROUP
   ד. הדפסת כל ה CONTACTS בתצורה עצית
   ה. מיון CONTACTS עפ"י שם


 

חלק א': תיאוריה, OODA

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

1. פקד בסיס אבסטרקטי - המינימום שמשותף ל-Contact ו-Contact Group (להלן: CG) הוא שלשניהם יש שם תצוגה כלשהו. ולכן, ניצור פקד אבסטרקטי כלשהו שממנו שניהם יירשו. שימו לב, הכוח האמיתי של יצירת מחלקות בסיס אבסטרקטיות יתברר בהמשך התשובה. כרגע, נציין שאנחנו עובדים לפי Abstract Factory Design Pattern או בשמה המיקרוסופטי המחודש Provider.

/* Busniss Logic Layer */
public abstract class BaseContact
{
  // DisplayName Property
  private string _displayName = "";
  public string DisplayName
  {
   get  { return _displayName;  }
   set { _displayName = value; }
  } 
}

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

2. ניישם את Contact באמצעות ירושה מ-ContactBase. נוסיף לו איזה תכונה לשם הדגמה בכדי להבדיל אותו מ-ContactBase. בדרישות הפרוייקט מצויין שצריך לשמור בו אי-מייל, אז באמת נוסיף לו Email property.
בנוסף לפי דרישת פרוייקט (4) סעיף א', נבנה Constructor ל-Contact שיקלוט DisplayName ו-Email.

/* Busniss Logic Layer */
public class Contact : BaseContact
{
  // Email Property
  private string _email;
  public string Email
  {
   get { return _email; }
   set { _email = value; }
  }
 
  // Constructor
  public Contact(string pEmail, string pDisplayName)
  {
   _email = pEmail;
   this.DisplayName = pDisplayName;
  }
}

3. ניישם את CG (דהלן: Contact-Group).  לפי דרישת פרוייקט (4) סעיף א', יהיה לו Contructor.

/* Busniss Logic Layer */
public class ContactGroup : BaseContact
{
 
  // Constructor
  public ContactGroup(string pDisplayName)
  {  
   this.DisplayName = pDisplayName;
  }
}

מבחינת היישום של סעיף (1) של דרישות הפרוייקט שלנו - סיימנו. להזכירכם את סעיף 1: “שתי ישויות - CONTACT , CONTACT-GROUP  (לטובת EMAIL)“. כעת נביט על דרישות פרוייקוט (2), (3) וסעיפים ב', ג' ו-ה' בדרישה (4). אנו יודעים כי: CG צריכה צריכה אפשרות להוסיף ולזרוק: CGים ו-Contactים. בנוסף, צריך שיהיה אפשר למיין את מבנה הנתונים CG לפי מפתח שמי כלשהו.

כעת נדבר נזכיר שSortedList היא בדיוק כמו ArrayList מהבחינה שנוכל להכניס לתוכה כל Object אחרי שנעשה לו Boxing, והערך המוסף שלה (ובמיוחד לתרגיל זה) הוא שניתן לסדר את ה-ArrayList לפי מפתח שנקבע.

נסכם, עלינו להוסיף מתודות שיאפשרו (ביחס ל-CG): להוסיף Contact, להוריד Contact, להוסיף CG ולהוריד CG. היות ו-Contact ו-Contact-Group שניהם יורשים מ-BaseContact זהו מצב אופטימלי ליצור Strongly typed Collection או במקרה שלנו BaseContact type SortedList. כלומר, דבר ראשון ניצור טיפוס נתונים פנימי שלנו שכל מטרתו בחיים היא לשמור SortedList היכולה לקבל רק ContactBase ויורשיו. מדובר על תחום השווה למאמר בפני עצמו, אבל נסתפק ביישום הזה.

/* Busniss Logic Layer */
/// <summary>
///     Strongly type SortedList for BaseContact
/// </summary>
/// <remarks>
///     Not generated by myGeneration/CodeSmith, written for demonstration.
///     in a real Strongly-typed collection you will implement all  Members of ArrayList.
/// </remarks>
public class ContactItems
{  
  private System.Collections.SortedList _contactItems = new SortedList();
  /* public Methods for dealing with Adding/Removing Contact-Groups/Contact */
  public void Add(BaseContact curContactBase)
  {
   _contactItems.Add(curContactBase.DisplayName, (object)curContactBase);
  }
 
  public void Remove(BaseContact curContactBase)
  {
   _contactItems.Remove(curContactBase.DisplayName);
  }
  // Get Count property
  public int Count
  {
   get { return _contactItems.Count; }
  }
  // Indexer
  public BaseContact this [int index]
  {
   get { return (BaseContact)_contactItems[index]; }
   set { _contactItems[index] = value; }
  }
}

 

בנינו את ה-Strony typed collection עם כל ה-Members שהיינו צריכים לתרגיל זה: אפשר להוסיף אך ורק ContactBase או יורשיו, ניתן להוריד אך ורק ContactBase או יורשיו, ישמנו מחדש את Count וכמו כן בנינו Indexer כדי שנוכל לגשת למערך הפנימי. באמת כל מה שעשינו זה Encupsulation ל-SortedList כך שיוכל לקבל אך ורק אובייקטים מהסוג שנקבע לו ויחשוף בדיוק מספיק פונקציונליות כדי שנוכל לעבוד איתו.

ניישם ונשכתב את ContactGroup כך שתכיל מערך פנימי של ContactItems כ-Property של ContactGroup:

/* Busniss Logic Layer */
public class ContactGroup : BaseContact
{
  private ContactItems contactItems;
  public ContactItems ContactItems
  {
   get { return contactItems; }
   set { contactItems = value; }
  }
 
  // Constructor
  public ContactGroup(string pDisplayName)
  {  
   this.DisplayName = pDisplayName;
  }
}
 

נבדוק שכיסינו את הדרישות הבאות: דרישות פרוייקט (2) ו-(3), וסעיפים ב' ו-ג' בדרישת פרוייקט (4).

דרישת פרוייקט 2 מבקשת מאתנו ש-CG תוכל להכיל CG ו-Contact. היות ושני המחלקות הללו יורשות מ-ContactBase ולכל CG יש ContactItems המכיל SortedList של ContactBase אז מילאנו דרישה זו.

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

דרישת פרוייקט 4 סעיפים ב' ו-ג' מדברים על כך שניתן יהיה להוסיף או להסיר CG או Contact מרשימה CG קיימת. היות ושני מחלקות אלו יורשות מ-ContactBase ולכל CG יש ContactItems המכיל SortedList של ContactBase ואחת מהאפשרויות היא להסיר ולהוסיף ContactBase ב-SortedList מילאנו דרישות אלו.

דרישת פרוייקט 4 סעיף ה' מדברת על מיון של CG לפי שמות התצוגה של ה-ContactItems שלה. היות ו-SortedList, שהוא טיפוס המידע הפנימי של ContactItems, מתמיין אוטומטית לפי מפתח - מילאנו דרישה זו כמו כן.

4. כל מה שנשאר הוא לממש את סעיף ד' של דרישת פרוייקט (4). והוא: תצוגה במבט עץ. ניצור Presentation Layer ובתוכה כל המימוש לתצוגת נתונים אלו. המימוש הפשוט ביותר לדרישה זו הוא דרך פקד תצוגה TreeView.

נדבר מעט על המבנה של שכבת התצוגה. כל אלמנט בשכבת התצוגה צריך אפשרות להפוך אותו ל-TreeNode (ענף בתצוגת העץ שלנו). ולכן, עלינו ליצור מחלקה שאחראית לתצוגה ל-Contact ול-CG. היות ויש לה תכונה משותפת אלינו לדאוג שהן ירשו מאותה מחלקה\ממשק. בדוגמה זו ישמתי ממשק שנורש ע“י abstract class וזה בתורו נורש ע“י פקדי התצוגה שלנו. צריך להזכיר שלא צריך לממש גם ממשק וגם abstract class, אלא אחד מהם. 

/* Presentation Layer */
public interface ITreeable
{
  TreeNode ReturnTreeNode();
}
 
abstract public class BaseContactPresentation : ITreeable
{
  abstract public TreeNode ReturnTreeNode();
}

ניישם את המחלקה הזו על ContactPresentation שתהיה אחראית על לממש את התצוגה של Contact: 

/* Presentation Layer */
public class ContactPresentation : BaseContactPresentation
{
  private Contact _contact;
  public ContactPresentation(Contact curContact)
  {
   _contact = curContact;
  }
  public override TreeNode ReturnTreeNode()
  {
   return new TreeNode(_contact.DisplayName);
  }
}

 

שימו לב, כל מה שעשינו זה לבנות קונסטרקטור שמקבל לתוכו Contact ומימשנו אפשרות להחזיר ענף בעץ אשר כתוב עליו את Contact.DisplayName. נעשה דבר דומה עם CG הרי כל CG הופכת בהכרח לענף בעץ לפי ContactGroup.DisplayName. אך, בנוסף לכך - נעבור על כל הפקדים ב-ContactItems ובהתאם לאיזה סוג האובייקט נשלח אותם לפקד המתאים שיחזיר לנו עבורם ענף בעץ.  

/* Presentation Layer */
public class ContactGroupPresentation :BaseContactPresentation
{
  private ContactGroup _contactGroup;
  public ContactGroupPresentation(ContactGroup curContactGroup)
  {
   _contactGroup = curContactGroup;
  }
  public  override TreeNode ReturnTreeNode()
  {
   // Create New TreeNode from DisplayName
   TreeNode curTreeNode = new TreeNode(_contactGroup.DisplayName);
 
   // Add SubTreeNodes from ContactGroup.ContactItems
   for (int i=0; i<_contactGroup.ContactItems.Count; i++)
   {
    BaseContact curBaseContact = _contactGroup.ContactItems[i];
 
    // If current BaseContact gotten from ContactItems is Contact
    if (curBaseContact.GetType() == typeof(Contact))
     // Add TreeNode from ContactPresentation of Current BaseContact
     curTreeNode.Nodes.Add( new ContactPresentation((Contact)curBaseContact).ReturnTreeNode() );
 
    // If current BaseContact gotten from ContactItems is ContactGroup
    if (curBaseContact.GetType() == typeof(ContactGroup))
     // Add TreeNode from ContactGroupPresentation of Current BaseContact
     curTreeNode.Nodes.Add( new ContactGroupPresentation((ContactGroup)curBaseContact).ReturnTreeNode() );
   }
 
 
   // return current TreeNode
   return curTreeNode;
  }
}
 

נציג דוגמה למימוש כל הקוד באפליקציה חלונאית (אשר קיים בה treeview בתצוגת עיצוב):  

/* Presentation Layer */
  public Form1()
  {
   InitializeComponent();
   ContactGroup _inbox = new ContactGroup("inbox");
   _inbox.ContactItems.Add(new Contact("
a@a.com","Justin"));
   _inbox.ContactItems.Add(new Contact("
b@b.com","Yony"));
   _inbox.ContactItems.Add(new Contact("
c@c.com","TTTIS"));
   _inbox.ContactItems.Add(new Contact("
d@d.com","Grandma"));
   _inbox.ContactItems.Add(new ContactGroup("my Living Enemies"));
   ContactGroup InLineContactGroup = new ContactGroup("myFamily");
   InLineContactGroup.ContactItems.Add(new Contact("
mom@family.com","mom"));
   InLineContactGroup.ContactItems.Add(new Contact("
dad@family.com","dad"));
   _inbox.ContactItems.Add(InLineContactGroup);
            
   ContactGroupPresentation inbox = new ContactGroupPresentation(_inbox);
   tvwtreeView1.Nodes.Add(inbox.ReturnTreeNode());
  }

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

 

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

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

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

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

השלב הראשון של אפיון-עיצוב עם הלקוח הוא אחד מהעקרונות של Agile Software developement, לעבוד עם הלקוח ולתת לו הרגשה שהוא מבין באמת כל מה שאנו הולכים לעשות. כל תוצר שהלקוח רואה הוא צריך להיות יכול להבין בצורה מלאה בלי להשתמש במונחים שזרים לו (כגון: ”שחקנים”, “שאילתות”, “פקדים” וכיו”ב).

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

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

הפורמט הנוכחי של המסמך הוא:

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

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

הפירוט פר Feature מציג תרשים UMLי אינטואטיבי מאוד שהוא סה”כ תמונה יפה. רואים בו את האנשים וכיצד הם מתקשרים ואיזה מקורות מידע וממשוק למערכת חיצוניות\פנימיות הם חלק מאותו תרשים. העובדה שהתרשים הזה הוא “תמונה יפה“ משחרר המון-המון-המון עבודה שבכל העולם היום משקיעים ב-UML. הבעיה הרצינית עם UML נובעת מכך שהוא כלי ולא סטנדרט. בשום מקום לא כתוב לאיזה רמת פירוט צריך להגיע בתרשימי UML. ברגע שמגיעים לסטנדרט ארגוני בנושא נשקיע כל-כך הרבה זמן לעמוד בו שההשקעה הזאת פשוט לא תשתלם. התרשים ה-UMLי במסמך הזה מיועד לכולם: המנהל שמבין כיצד אנו נבנה את התוכנה שלו, המזכירה שרואה את מקומה בארגון ומציעה שינויים, התוכניתן שצריך לבנות ובשלוש שניות מבט הבין מה התהליך שהוא בונה בקוד, והתוכניתן שצריך לתחזק את המערכת ומבין בתוך אותם שלוש שניות מה הולך ומי-נגד-מי.

בנוסף הפירוט פר Feature מכיל רשימה טקסטואלית לחלוטין של Sub-Features של ה-Features הגדולים יותר. כאן זה המקום שנרשום את ה-Busniss Logic שלנו.

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

3. ERD של המסד נתונים. בנקודה שאתה כבר יודע איזה שדות יש לך במסך ואיזה כותרות ואיזה הכל - אין שום בעיה לכתוב ERD. עכשיו, בניגוד לכל שאר המסמך הזה שמלבד שהוא “מפת-דרכים“ לפרוייקט שלנו ונועד בעיקר ככלי אינטואטיבי להבנת האפליקציה - הERD אצלי הוא כלי עבודה חשוב ומהותי. כרגע אני כותב את ה-ERD בתוך תוכנת עיצוב מסדי נתונים בשם Erwin והוא מופיע במסמך עיצוב כקובץ בתוך מהמסמך. הסיבה היא שבחרתי Data-Driven development כצורת המחשבה שתקשר בין ה-Features האינטואטיביים לקוד של הפרוייקט.

 

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

א) Features במערכת:

1. יצירת איש-קשר חדש בטבלת אנשי-קשר.

2. יצירת קבוצת-קשר חדשה בטבלת קבוצות-קשר.

3. עריכת פרטי איש-קשר קיים בטבלת אנשי-קשר.

4. עריכת פרטי קבוצת-קשר קיימת בטבלת קבוצות-קשר.

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

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

6. תצוגת אנשי-קשר בתצוגת-עץ.

 

ב) ERD של המערכת - כתיבת Entity-Reletionship dIagram לשלושת הטבלאות (אנשי-קשר, קבוצות-קשר, וטבלה המקשרת בין אנשי-קשר וקבוצות-קשר). אחר כך הייתי מוסיף את הפונקציות שנובעות מה-Features. למשל אם עובדים עם Stored Procedures הייתי מוסיף לטבלת אנשי-קשר את contacts.AddNew ו-contact.EditExisting, לטבלת קבוצות-קשר הייתי מוסיף את contact_groups.AddNew ו-contact_groups.EditExisting, לטבלה שמקשרת בין שתי הטבלאות הללו הייתי יוצר את relation.AddRelation ואת relation.RemoveRelation. כמו כן הייתי מוסיף SP שמטרתה להחזיר טבלה שתתורגם בקלות ל-TreeView.

ג) כתיבת המסכים ב-Dot net forms ומקשר ביניהם ל-Stored Procedures. על חלק זה אפשר להרחיב ואני מבטיח לעשות זאת.

 

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

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

 

ברכות,

ג'סטין-יוסף אנג'ל

 

היסטורית שינויים:

24.7.2005 - בחלק א': יישום Composite Design Pattern, תודה לייוניי.

25.7.2005 - בחלק א': כתיבת Presentation layer. הוספת Strongly-typed collection.

אורקל ודוט נט - המציאות מוזרה מכל דמיון

שלום לכולם,

במאמר הזה אני אדבר על החיבור המוזר של אורקל ודוט נט, מנקודת המבט של מפתחים המשתמשים System.Data.OracleClient.

קדם דבר

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

מה זה אורקל ביחס לסיקוול סרבר(Sql server)?  פחות או יותר אותו דבר. יש הבדלים, אבל בוא נגיד שהם יותר למומחי מסדי-נתונים שזה העבודה שלהם ולא למפתחים. לעומת זאת יש הבדלים שחשובים לנו: באורקל עובדים עם PL/SQL ובסיקוול עובדים עם T-SQL, הממשק הנפוץ של סיקוול סרבר הוא ה-Enterprise Manager שהוא כלי גרפי והממשק הנפוץ של אורקל הוא חלון דוס מתוחכם (הרקע בלבן והטקסט בשחור ולא כמו בדוס שזה להפך), אההה וכן - מיקרוסופט לא פיתחה את אורקל. (המאמר אינו עוסק בהבדלים בין אורקל וסיקוול סרבר ואין לקחת את פיסקה זו אפילו כעל קצה המזלג)

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

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

חלק א': Data Providers (או: אופס', בנינו למתחרים מוצר יותר טוב)

כיום עם הדוט-נט Framework נשלח Namespace בשם System.Data, והוא בין השאר מכיל את ה-Data Providers של דוט נט. ה-Data Providers הללו מאפשרים לנו להתקשר למסדי הנתונים. דוט נט פריימוורק נשלח עם שלושה Data Providers כברירת מחדל: סיקוול סרבר (SqlDataProvider), אורקל (OracleDataProvider) ואחד לכל השאר (OleDbProvider).

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

משהו מאוד מעניין לשים לב עליו הוא בדיקות מהירות (BenchMarking) שבוצעו על שלושת ה-Native Data Providers. חיפשתי במשך כמה שעות מאמר שקראתי לפני כשנתיים ב-Dotnetjunkies.com המראה Preformance testing של שלושת ה-DataProviders ומראה כיצד בכל הסיטואציות שנבדקו לאורקל Data Provider יש ביצועים מהירים יותר. חשוב להדגיש, מיקרוסופט בנו את ה-Data Provider של המתחרים ככה שבשילוב עם מסד הנתונים אורקל - הוא יותר מהיר מה-Data Provider של סיקוול סרבר בשילוב עם סיקוול. (חשוב להדגיש שחיפשתי את המאמר, לא מצאתי, שלחתי אי-מייל למנהלי האתר ואם וכאשר הם יחזרו אליי עם קישור אני אשים אותו כאן. אגב אתם מוזמנים לחפש בדיקות מהירות של ADO.net ל-Data Providers השונים ולהוסיף בתגובות למאמר)

חלק ב: עבודה עם קידודים (או: “מה קורה שמנסים לכתוב עברית במסד נתונים מערב-אירופאי”)

אורקל (בדומה לסיקוול סרבר) עובד עם קידודים שונים לשפות שונות. באורקל הקידודים הללו נקראים CharcterSets. ישנם שלושה CharcterSets נפוצים: utf-8 (יוניקוד), IW8ISO8859P8 (עברית ואנגלית), WE8ISO8859P1 (אנגלית). דוט נט בתוך הפריימוורק עצמו, בתוך ה-CLR, עובד עם יוניקוד.

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

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

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

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

  public string encodingConvert(string from_encoding,string to_encoding,string src)
  {
   Encoding fromEncoding = Encoding.GetEncoding(from_encoding);
   Encoding toEncoding = Encoding.GetEncoding(to_encoding);
   return toEncoding.GetString(fromEncoding.GetBytes(src));
  }

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

  public string encodeheb(string src)
  {
   return encodingConvert("windows-1255","ISO-8859-1",src);
  }

  public string decodeheb(string src)
  {
   return encodingConvert("ISO-8859-1","windows-1255",src);
  }

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

ברגע שניישם את הפונקציות הללו בקוד שלנו, נצטרך להתחיל ליצור פונקציות שיודעות לתרגם את הפקדים הרלוונטים. למשל בעת החזרת נתונים מאורקל נצטרך לדאוג לבצע decodeheb על כל DataCell בכל DataRow בכל DataTable בכל DataSet וגם לבנות מעטפת לפונקציות של OracleDataReader. בעת שליחת נתונים לאורקל דרך שאילתות נצטרך לדאוג כי הן יעברו במקום מרכזי שבו יהיה ניתן קדם שליחתם לאורקל לדאוג שהם יעברו לקידוד שאורקל יכול לעבוד איתו באמצעות פונקציה encodeheb (עקרונית, לעבור על OracleParameter.Value ולדאוג להעביר אותו לקידוד המתאים).

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

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

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

 

חלק ג': ORA-01036: illegal variable name/number  (או: “הודעת שגיאה ג'נארית שלא אומרת כלום!“)

למי מכם שצברו ניסיון בעבודה עם System.Data.OracleClient הבינו עד עכשיו קטע מאוד מעניין: מיקרוסופט לא היו כזה סגורים על כל הקטע של Bind Variables (המקביל של אורקל ל-SqlParameter של סיקוול) ובחלק מהמקומות פשוט לא דאגו להודעות שגיאה נורמליות.

ההודעת שגיאה אשר כתובה למעלה היא הודעת שגיאה של אורקל. אורקל זורק את השגיאה הזאת כאשר דוט נט מצהירה בצורה בלתי-זהירה על Bind Variables. מה זה “בלתי-זהירה”? מה צריך לבדוק? מה צריך לתקן?

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

הנה האפשרויות שאני נתקלתי בהן:

1. השם של ה-Bind Variable גדול מ-31 תווים.

2. אי-הוספת “:” בהצהרה על שם ה-Bind Variable (בדומה לשטרודל של סיקוול סרבר).

3. אם הצהרנו על Bind Variable ולא השתמנו בו.

4. סינטקס קלול ליד שם הפרמטר - למשל, לשים רווח לפני\אחרי שם פרמטר או שם פרמטר עם רווחים.

5. לא הוספנו ל-OracleCommand מאפיין CommandType.

6. הצהרה על סוג פרמטר לא נכון ביחס לעבודה איתו בשאילתות או PL/SQL (הצהרה על מחרוזת ולהשוות למספר, להעביר LONG במקום NUMBER בתוך פונקציה וכיו”ב).

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

 

חלק ד': ORA-00100 error: maximum open cusors (או: “הפקודות שסירבו למות”)

שגיאה מאוד פשוטה שנגרמת מכך שלכל שרת אורקל יש מספר מקסימלי של Cursors שהוא רשאי לפתוח בזמן נתון. אם עברנו את הגבול הזה - כנראה מאוד שעשינו משהו לא נכון. (או שיש לנו מעל 100+ פניות למסד הנתונים פר שנייה ביחס לערך ההתחלתי של המקסימום Open cursors).

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

אופציה שנייה, לא עשיתם Dispose ל-OracleCommand שלכם. בכדי להחזיר רשומות דרך PL/SQL בדוט נט צריך לעבוד עם מה שנקרא Ref Cursors.  מה שקורה הוא שנשלחות תוצאות שאילתא מאורקל בטיפוס הנתונים הפנימי שאורקל עובד איתו. הטיפוס נתונים הזה צריך בסוף להיסגר אחרת הוא נחשב פתוח (עמוק, אני יודע). יש גבול לכמה תוצאות שאילתות פתוחות אפשר להחזיק בו-זמנית בשרת אורקל. אם שלחנו Ref Cursors מ-PL/SQL לדוט נט באמצעות OracleCommand חובה עלינו לבצע Dispose ל-OracleCommand ברגע שהיא כבר לא בשימוש. אסור לחכות ל-GC שיעשה את זה.

אופציה שלישית, לא עשיתם שום דבר רע. האפליקציה שלכם גדלה והגיע למצב שצריך לדאוג ל-Oracle Scalability. דברו עם ה-DBA שלכם, אבל בעקרון מספיק לשנות את max_open_cursors בקובץ ההגדרות של השרת אורקל. מניסיון מכאיב עם DBA, אל תעלו את המקסימום לפי 5 בלי להגיד ל-DBA :)

 

חלק ה': פריסת לקוח אורקל (או: “איזה מזל שצריך לעשות את זה רק פעם אחת!”)

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

האפשרות הראשונה היא להתקין על המכונה הזאת Oracle Client שזאת חבילת תוכנות שאפשר להוריד מהאתר של אורקל. לקנפג את ה-TNS_NAMES והכל מוכן. למיטב ידיעתי לכל גירסה של אורקל שאינו 10/11 אין שום אפשרות אחרת.

אם עובדים עם אורקל 10/11 יש מה שנקרא Oracle Instant Client. מדובר על קבוצה מאוד קטנה של DLLים שמאפשרים התחברות למסד אורקל. כמה התהליך של התקנת Instant Client:

1. הורדת Oracle 10g/11 Instant Client מאתר ההורדות של אורקל.

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

3. העתקת TNS_NAMES.ora ו-Sqlnet.ora לספריה כלשהי במכונה. (לא לשכוח קנפג את TNS_NAMES).

4. להוסיף Enviorment variable על המכונה בשם TNS_ADMIN שיכוון למיקום של הקובץ.

5. לתת למשתמשי ה-IIS הרלוונטים גישת Read & Read and execute & List content על הספרייה שבו שמנו את ה-TNS_NAMES.ora. ייתכן וגם יהיה צורך לתת למשתמשי ה-IIS הרלוונטים הרשאה לקרוא את ה-DLLים שפרסנו בסעיף 2 (משתנה לפי גירסת מערכת ההפעלה של חלונות ועדכוני האבטחה).

 

לסיכום

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

 

ברכות,

ג'סטין-יוסף אנג'ל

Struct ו-Class -ההבדלים והכוח הטמון בשוני ביניהם

שלום לכולם,

קדם דבר

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

public class ClassName
{
// ...
}

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

public struct StructName
{
// ...
}

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

 

חלק א': על Stack ו-Heap, ועל Refrence type ו-Value type (או: “אני אמיתי ואתה לא!“)

נחזור ונכיר כמה מושגי יסוד: Value types ו-Reffrence types. כידוע, כל אובייקט במערכת יורש מ-System.Object, אך יש קבוצה קטנה של בעלי סגולה שיורשים גם מ-System.ValueType ומכאן מתחילים כל ההבדלים. נזכיר כעת כמה ValueTypes שהם סלברטאים: int, bool, char ו-enum. עכשיו נראה כמה Refrence Types שגם הם סלבריטאים: System.Web.UI.DropDownList, System.Windows.Forms.CheckBox ו-ArrayList. מניסיון קודם אנחנו רואים בבירור שיש הבדלים כלשהם.

אחרי שהראינו כי באופן אינטואטיבי קיים הבדל כלשהו, ניכנס עוד יותר לעומק ונדבר על Stack ו-Heap. ה-Stack וה-Heap הם מקומות המוקצים בזכרון המחשב ובו נמצאים האובייקטים שלנו.  כל Refrence type שניצור יווצר על ה-Heap, ובתוך בלוק הקוד שלנו יווצר לנו משתנה המצביע על אותו מקום ב-Heap רק שההפנייה הזאת ל-Heap יושבת ב-Stack. במילים פשוטות, שניצור DropDwonList הוא נוצר למעשה ב-Heap ושאנו עובדים עם אותו DropDownList אנו למעשה עובדים עם הפנייה שיושבת ב-Stack שמפנה לאותו מקום ב-Heap. מהסיבה הזאת אפשר ליצור מיליון הפניות ב-Stack לאותו מקום ב-Heap. נראה בדוגמה על מה דיברנו כרגע: 

public void someProcedure()
{
  System.Web.UI.DropDownList firstDDL = new DropDownList();
  System.Web.UI.DropDownList secondDDL = firstDDL;
 
  firstDDL.Items.Add(new ListItem(“SomeItem));
 
  // print how many items in firstDDL
  Response.Write( firstDDL.Items.Count + “ items in first DDL“ );
  // print how many items in secondDDL
  Response.Write( secondDDL.Items.Count + “ items in second DDL“ );
}

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

עכשיו נדבר על Stack. הרעיון מאחורי Stack זה לשמור מידע כמה שיותר זמין לתקופת הזמן שאנו עובדים איתו. ה-Stack באופן יחסי קטן באופן ניכר מה-Heap מהסיבה היא שזה הזכרון שאיתו רוב הזמן המעבד עובד איתו וככל שהוא יותר “ממוקד” ככה העבודה מהירה יותר. זה מתקשר ל-ValueTypes בכך שכל ValueType נשמר רק על ה-Stack ואין טיפת קשר ל-Heap. היות ואנו לא עובדים עם הפניות אנו תמיד עובדים עם אובייקטים ממשיים. ניישם את הדוגמה שהוזכרה למעלה על int32: 

public void someOtherProcedure()  { /* Changed on 30.7.05, Thanks Eran */
{
int firstInt = 1;
int secondInt =  firstInt;
 
  secondInt = secondInt + 1; // (secondInt += 1;) 
 
  // print first Int
  Response.Write(”first Int32 is: “ + firstInt.ToString() );
  // print second string
  Response.Write(”second Int32 is: “ + secondInt.ToString() );
}

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

 

חלק ב': ואיך כל זה קשור למבנים ומחלקות? (או: “נו, תכלס', למה שיהיה אכפת לי?”)

מבנה הוא ValueType (בדומה למחרוזת, מספרים, בוליאנים ו-Enums) ומחלקה היא Refrence Type (פחות או יותר כל השאר). נתחיל לדבר על הבדלים מבחינת תכנות:

1. אי-חובת כתיבת Constructors: (או: “אותי אתה לא צריך לאתחל“)

למחלקה תמיד חובה שיהיה Constructor אחד לפחות. כאשר כל ה-Constructorים של מחלקה הם Private - לא ניתן לממש את המחלקה. נראה דוגמה:  

public class myClass
{
  private myClass()
  { }
}
 
myClass curClass = new myClass(); // Throws exception

למעשה את myClass מהדוגמה אי-אפשר לממש. אין לו שום Constructor שבאמת ניתן דרכו לממש את myClass.

לעומת זאת, מבנה תמיד תמיד תמיד אפשר לאתחל. תכתבו Constructor, אל תכתבו Constructor, תכתבו שה-Constructor הוא Private, למי אכפת?  

public struct myStruct_NoConstructor
{
}
 
public struct myStruct_PrivateConstructor
{
  private myStruct_PrivateConstructor(int x)
  { }
}
 
public struct myStruct_SomeConstructor
{
  public myStruct_SomeConstructor(int x)
  { }
}
 
 
// No Exceptions will be thrown
myStruct_NoConstructor struct1 = new myStruct_NoConstructor();
myStruct_PrivateConstructor struct2 = new myStruct_PrivateConstructor();
myStruct_SomeConstructor struct3 = new myStruct_SomeConstructor();
 

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

2. אי-חובת איתחול (או: “ברגע שדיברת אליי, אני כבר פה“)

בזמן שניתן לממש כל מבנה בלי קשר לסטטוס ה-Constructor שלו, לא חייבים בכלל לאתחל אותו. מספיק לתת “הפנייה” למבנה והוא כבר מאותחל. ברגע שנכתוב הפנייה למבנה נוכל לגשת לכל ה-Public members שלו.  

public struct myStruct
{
  public int x;
}
 
// We do not initliize myStruct
myStruct curStruct;
 
// Change public X int value (no exception is thrown)
curStruct.x = 1;
 
  // Print X (no exception is thrown)
Console.Write(curStruct.x);

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

ברור גם למה זה קורה, הרי מבנה יושב ב-Stack שברגע שאנו מגדירים אובייקט שיושב שם ברור שלא מדובר באיזה הפנייה ל-Heap, אלא באובייקט לפני עצמו ולכן ה-CLR ישר שם מקום המיועד למבנה ב-Stack. כנ”ל ברור יקרה גם עם  int, enum ושאר Value Types.

3. מבנה לא יכול להיות null (או: “אני תמיד איתכם ברוחי“)

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

myStruct curStruct;
if (curStruct==null) { } // Throws Exception

4. אין הורשה למבנה או ממבנה (או: “נדוניה וירושה”)

כל מה שהוא Value Type יורש בהכרח מ-System.ValueType שנותן לו את כל היכולות שהזכרנו ונזכיר. אך, מעבר ללרשת מ-System.ValueType שקורה באופן שקוף לנו, לא ניתן לבצע ירושה על ValueTypes. כלומר מבנים לא יכולים לרשת מבנים\מחלקות אחרות, ולא יכולים להיות אובייקטים מהם יורשים. דוגמה ידועה לזה היא שלא ניתן לרשת int וליצור SuperInt עם הרבה יותר פונקציונליות.

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

נחדד את ההבדלים בין מבנה למחלקה בנושא ירושה: כל מבנה הוא Sealed (לא ניתן לרשת ממנו והוא לא יכול לרשת), אי-אפשר להצהיר על Abstract struct (הרי אם אין ירושה ממבנה זה מיותר),  לא ניתן להצהיר על Protected members (הרי אם לא ניתן לרשת ממנו אז מה הטעם לכתוב משתנים שרק מי שיורש ממנו יכול לגשת אליהם?), ומבנה לא יכול לעשות Override למתודות שלא נורשות מ-System.Object.

5. אסור Destructor (או: “אני הולך לבד הביתה”)

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

הנה דוגמה למחלקה עם Destructor שיתקמפל, ודוגמה למבנה עם Destructor שלא יתקמפל:

// Will not throw compile error
public class myClass_Destrcutor
{
  public myClass_Destrcutor()
  { }
 
  ~myClass_Destrcutor()
  { }
}
 
// Will throw Exception
public struct myStruct_Destrcutor
{
  ~myStruct_Destrcutor()
  { }
}

שננסה לקמפל את myStruct_Destrcutor נקבל את השגיאה הבאה: Only class types can contain destructors.

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

6. אופן ההשוואה בין מבנים ומחלקות (או: “במחלקות זה האריזה, במבנה זה התוכן“)

כאשר נשווה בין שתי מחלקות מה שיתרחש למעשה זה השוואה של “האם ההפניות בזכרון מפנות לאותו מקום?”

public class myClass
{
  public myClass()
  { } 
}
 
myClass firstClass = new myClass();
myClass firstClass_SamePointer = firstClass;
myClass secondClass = new myClass();
 
bool firstTest = (firstClass== firstClass_SamePointer); // Is true
bool secondTest = (firstClass == secondClass); // Is False
 
Console.WriteLine(firstTest.ToString() + " " + secondTest.ToString());

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

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

public struct myStruct_Equels
{
  public int x;
 
  public override bool Equals(object OtherObj)
  {
   myStruct_Equels otherMyStruct_Equels  = (myStruct_Equels) OtherObj;
   return otherMyStruct_Equels.x == x;
  }
 
}

יצרנו מבנה שעושה Override ל-Equels ובודק אם ה-X של מופע אחר של אותו מבנה שווה ל-X שלו.

myStruct_Equels firstStruct;
firstStruct.x = 1;
 
myStruct_Equels secondStruct;
secondStruct.x = 1;
 
Console.WriteLine((firstStruct.Equals(secondStruct)).ToString());

יצרנו שני מבנים מאותו סוג, הכנסנו להם אותו ערך ל-X ובדקנו אם הם שווים. התוצאה תהיה True. אם נשנה את הערך X של אחד מהם - התוצאה תהיה False.

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

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

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

public struct myStruct_Equels
{
  public int x;
}

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

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

7. התייחסות פנימית (או: “אני בכלל הוא”)

תשומת לב במיוחד לסעיף הזה (והבא) בבקשה כי עליהם מבוססת הדוגמה בהמשך.

נביט שנייה על myClass: 

public class myClass_ThisIsOtherThis
{
  public myClass_ThisIsOtherThis()
  {
   this = new myClass_ThisIsOtherThis(); // Throws exception
  }
}

בדוגמה הזאת ניסינו בתוך פונקציה כלשהי (במקרה היא ה-Constructor) להגיד this שווה משהו אחר. במחלקהזה בלתי אפשרי. הסיבה היא שבמחלקה ה-this הוא רק הפנייה למקום מאוד ספציפי בזכרון. כחלק מרעיון ה-managed code (קוד שמנהל לנו שימוש בזכרון) לא ניתן לעשות את זה. ולכן הקומפיילר זורק לנו שגיאת this is readonly.

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

public struct myStruct_ThisIsOtherThis
{
  private void myStruct_ChangeThis()
  {
   this = new myStruct_ThisIsOtherThis(); // Will not throw exception
  }
}
 
public struct myStruct_ThisIsOtherThis
{
  public int x;

  public myStruct_ChangeThis(int x)
  {
     this = new myStruct_ThisIsOtherThis(x+1); // Will not throw exception
  }
}

8. אתחול משתנים פנימיים (או: “אני בכלל לא פה”)

כאשר אנו בונים Members למחלקה הם מקבלים ערך של ברירת מחדל כאשר אנו מאתחלים את המחלקה (וכפי שידוע, לא ניתן לגשת למחלקה בלי שתהיה מאותחלת או בלי שה-Memebers יהיו סטטיים). נסביר בפשטות, אם נרשום משתנה פנימי למחלקה,  ברגע אתחול המחלקה המשתנה הפנימי יקבל ערך ברירת-מחדל. למשל מספר יקבל 0, מחרוזת תקבל “”, בוליאני יקבל 0 וכך הלאה. נדגים זאת: 

public class myClass_ValueOnInit
{
  public bool innerBool;
  public int innerInt;
  public string innerString;
 
  public myClass_ValueOnInit()
  {
   Console.WriteLine(innerBool.ToString());
   Console.WriteLine(innerInt.ToString());
   Console.WriteLine(innerString.ToString());
  }
}
myClass_ValueOnInit myClass = new myClass_ValueOnInit();

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

public class myClass_ValueOnInit
{
  public bool innerBool = false;
  public int innerInt = 0;
  public string innerString = "";
 
  public myClass_ValueOnInit()
  {
   Console.WriteLine(innerBool.ToString());
   Console.WriteLine(innerInt.ToString());
   Console.WriteLine(innerString.ToString());
  }
}

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

נדגים מה קורה במבנה. נשנה את המחלקה למבנה: (באמצעות החלפת ה-“Class“ ב-“Struct“)

public struct myStruct_ValueOnInit
{
  public bool innerBool;
  public int innerInt;
  public string innerString;
  public myStruct_ValueOnInit(object blah)
  {
   Console.WriteLine(innerBool.ToString());
   Console.WriteLine(innerInt.ToString());
   Console.WriteLine(innerString.ToString());
  }
}
 
// Throws Exception
myStruct_ValueOnInit myStruct = new myStruct_ValueOnInit(new object());

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

public struct myStruct_ValueOnInit
{
  public bool innerBool;
  public int innerInt;
  public string innerString;

}

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

9. עוד הבדלים

לא חסרים עוד הבדלים בין מבנים למחלקות: ניתן לשלוח מבנה כ-ref או out בפונקציות פנימיות, לא ניתן לנעול מבנה (כדי שהוא יהיה thread-safe), וכיו”ב. אתם מוזמנים להמשיך לחקור את הנושא.

 

חלק ב': ייעוד המבנה (או: “עף כמו דבורה, עוקץ כמו פרפר”)

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

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

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

 

חלק ג': דוגמה לשימוש נכון במבנה (או: “הלכתי לאיבוד, מה הקורדינטות של מיקומי הנוכחי?”)

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

- קורדינטת ציר אחד (ציר X בלבד)

- קורדינטית שני צירים (צירי X,Y בלבד)

- קורדינטת שלוש צירים (צירי X,Y,Z)

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

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

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

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

public class myCord
{
  public int x;
  public int y;
  public int z;
 
  public myCord(int x)
  {
   this.x = x;
  }
 
  public myCord(int x,int y)
  {
   this.x = x;
   this.y = y;
  }
 
  public myCord(int x,int y, int z)
  {
   this.x = x;
   this.y = y;
   this.z = z;
  }
}

(לצורך דיון לא ראיתי לנכון לסבך את הקוד עם Properties).

לפי מה שראינו המחלקה למעלה שקולה למחלקה הבאה:

public class myCord
{
  public int x = 0;
  public int y = 0;
  public int z = 0;
 
  public myCord(int x)
  {
   this.x = x;
  }
 
  public myCord(int x,int y)
  {
   this.x = x;
   this.y = y;
  }
 
  public myCord(int x,int y, int z)
  {
   this.x = x;
   this.y = y;
   this.z = z;
  }
}

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

לפי מה שעשינו עד כה ראינו כי אנו צריכים טיפוס נתונים כלשהו שיכול להכיל את כל השלושת הנתונים בלי לאתחל אותם - תכונה ידועה של מבנה!

public struct myCord
{
  public int x;
  public int y;
  public int z;
 
  public myCord(int x)
  {
   this.x = x;
  }
 
  public myCord(int x,int y)
  {
   this.x = x;
   this.y = y;
  }
 
  public myCord(int x,int y, int z)
  {
   this.x = x;
   this.y = y;
   this.z = z;
  }
}

קיבלנו מצב שבו לא יהיה שום Overhead שאינו נדרש. אין המרות, אין משתנים שלא בשימוש, אין תנאים. זה לא הרבה יותר טוב?

(במצב אמיתי גם היינו בונים Properties שהיו מגבילות גישה לנתוני Y,Z שאינם מאותחלים)

 

לסיכום

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

Assemblies ו-Strong named Assemblies - המדריך למדוע וכיצד

שלום לכולכם,

כולנו עובדים עם Assemblies (להלן: אסמבליס ברבים, אסמבלי ביחיד). גם אם אתמול התחלתם לעבוד בדוט נט ובניתם רק אפליקציה לדוגמה - אתם עבדתם עם אסמבלי.  בואו כולנו נפתח עכשיו Visual studio, נבחר לפתוח פרוייקט חדש ונקמפל (נעשה build). מה שכרגע קרה זה שהקומפיילר בנה לכם אסמבלי. אם בנינו אפליקציית web אז קיבלנו בספריית ה-bin קובץ myProject.dll, ואם בנינו אפליקציית winform/console נקבל בספריית ה-bin קובץ myProject.exe.

א. מהן אסמבליס?

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

דיברנו בנפנוף-ידיים על איך מקבלים אסמבלי, אז בואו נדבר על זה עוד קצת.כל קוד בדוט נט ללא שייכות לשפת התכנות מתקפל בסופו של דבר לקוד MSIL. קוד ה-MSIL הזה יושב בתוך קבצי PE (גם: Portable executable). אל תדאגו אם בחיים לא ראיתם כזה סוג קובץ, אם אתם עובדים עם Visual studio.net סביר להניח שבחיים גם לא תראו. הקבצי PE הללו שמכילים קוד MSIL בחיים לא יוכלו לרוץ אלא אם כן הם יהיו קשורים ל-Assembly manifest (להלן: מניפסט). אותם קבצי PE כברירת מחדל אינם מכילים מניפסט.

עוד חלק מהאסמבלי הוא ה-Type metadata. באנגלית Metadata זה - מידע שמתאר מידע. אז Type metadata זה מידע שמתאר טיפוסים. ב-#C טיפוסי נתונים הם המחלקות השונות שלנו. כלומר, Type metadata זה המידע שמתאר את המחלקות השונות שלנו. ה-Type metadata מכיל ממש את ה"כותרות" של המחלקה: האירועים שהמחלקה חושפת, המשתנים הפנימיים, ה-Properties שהמחלקה חושפת, המתודות של המחלקה (והפרמטרים שהיא מקבלת ומחזירה, האם היא סטטית או לא), ומאיזה מחלקה הטיפוס יורש. הנ"ל גם תקף לכל חלק אחר מה-assembly שיורש מ-System.object ולא רק למחלקות, למשל גם לממשקים ול-Enums.

פתחתי את כלי ה-ilsdam, שנותן לנו לבחון אסמבלי לאחר שהתקמפלה (כמו ה-Reflector המפורסם, רק יותר מאותגר ומגיע עם הפריימוורק). בחרתי להביט על System.dll והתחלתי לדפדף בפנים:

image

כל זה  (יותר נכון הרוב) זה Type metadata. בתמונה לעיל אפשר לראות כאן פרטים של מחלקת System.Timers.Timer. הכל נמצא כאן: החל ממשתנים פנימיים, Properties שהמחלקה חושפת, מתודות שהמחלקה חושפת (עם הפרמטרים שהיא מקבלת ומחזירה), המחלקה ממנה היא יורשת, הממשקים שאותה היא מממשת ועוד. כל זה הוא ה-Type metadata.

קבצי ה-PE שהזכרנו קודם לכן מכילים שלושה חלקים: נקודת כניסה (PE header), קוד MSIL, וה-Metadata של האובייקטים בקובץ. ה-Metadata של האובייקטים שלנו בתוך האסמבלי למעשה נשמר בתוך קבצי ה-PE.

אז אם נחשוב לרגע על איך פועלים למעשה כל ה-Reflectorים וה-ildasmים למיניהם שהם מראים לנו כזה תפריט ניווט. הם לא פותחים את קוד ה-MSIL בתוך האסמבלי, הם רק קוראים את המידע מתוך ה-metadata ומציגים לנו אותו כמו שהוא. עד כאן עם Type metadata. (אגב, רק לתת דוגמה עד כמה מסובך כל הסיפור הזה, בשפות כמו VB.net ו-#C יש הגבלה להורשה ממחלקה אחת בלבד, אבל ה-Type metadata גם יודע למשל לתאר הורשה ממספר מחלקות).

עוד חלק מכל אסמבלי הוא ה-Resources (להלן: משאבים). המשאבים הנפוצים של אפליקציות הם קבצים שגררנו לתוך עץ הפרוייקט ב-Visual studio (כגון תמונות וקבצי טקסט), ובנוסף משאבים שאחראים על שינוי תצוגה לתרבויות שונות. נציג כאן דוגמה של הראשון - קובץ תמונה בתוך פרוייקט. נגרור תמונה לתוך עץ הפרוייקט ב-visual studio ונרצה לראות שהוא באמת בתוך האסמבלי שלנו ולא בתוך איזו תיקייה מסתורית איפהשהו על ההארד-דיסק. איתרע מזלנו וה-ildasm אינו תומך בהצגת משאבים, אז נשתמש ב-Reflector:

image

והנה, אנו יכולים לראות באמצעות ה-reflector שבאמת בתוך ה-Reflector קיים קובץ התמונה שגררתי לתוך הפרוייקט. שאר הקבצים בתוך ספריית ה-Resources הם כמו שהזכרתי קבצים שיוצרו אוטומטית ע"י Visual studio למקרה ונרצה לשנות תכונות של האפליקציה בהתאם לתרבות בה היא מורצת (למשל, אם נרצה שבתרבות הישראלית-עברית ניתן יהיה להגדיל חלון ותרבות הצרפת-צרפתית לא יהיה ניתן).

אז מה יש לנו עד עכשיו בתוך אסמבלי: קבצי PE עם קוד MSIL, משהו בלתי מוכר בשם מניפסט, Type metadata, ומשאבים.

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

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

בנוסף המניפסט מכיל שני הפניות חשובות: Type metadata reffrence ו-Assemblies reffrence. ה-Type metadata reffrence משמש להגיד למי שטוען את האסמבלי "במקום הזה והזה תוכל למצוא את טיפוסי הנתונים שאיתם אני עובד". לרוב ה-Type metadata reffrence יפנה על האסמבלי עצמה ל-Type metadata שהיא חלק מהאסמבלי. בתקופה שלפני דוט נט שכולנו עבדנו רכיבי Com היו המון בעיות סנכרון של רכיבים מול ה-Type libaries שלהם. למזלנו, כברירת מחדל האסמבלי מכילה את המידע שמתאר את הטיפוסים איתה היא עובדת. (רוב המקרים בדוט נט בהם ה-Type metadata reffrence לא יפנה לאסמבלי עצמה היא באמת בעבודה עם רכיבים מהדור הישן).
ה-Assemblies reffrence גם מאוד חשוב. הוא זה שבא ואומר לאיזה אסמבליס אחרים האסמבלי הזו קשורה. למשל, אם אנחנו כתבנו קוד שמצייר פרח על המסך, כאשר נקמפל את הקוד אנו נקבל כחלק מהמניפסט הפנייה ל-System.Drawing.
עוד משהו שהמניפסט מכיל הוא מידע שמזהה את האמסבלי - שם האסמבלי, גירסת האסמבלי, התרבות אליה שייכת האסמבלי ומפתח. כל אלו ביחד נקראים Strong name. עוד נדבר על הנושא הזה בהמשך. אבל כרגע נזכור שלכל אסמבלי יש את ארבעת הנתונים הללו במניפסט שלה, ואם לא הגדרנו אותם הם מקבלים ערכי ברירת מחדל.

בוא נסכם מה מכילה כל אסמבלי:

1.  קבצי PE המכילים קוד MSIL. זה למעשה הקוד המקומפל אותו כתבנו.

2. Type metadata. זה למעשה תיאור כל האובייקטים בכלל וכל מחלקות בפרט. (צריך לזכור שהמידע הזה לרוב יושב בתוך קבצי ה-PE שלנו)

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

4. מניפסט האסמבלי (חובה) שמכיל את: רשימת קבצי ה-PE והמשאבים באסמבלי, Type metadata reffrence שלרוב יצביע בחזרה על ה-type metadata מסעיף 2, Assemblies reffrence שיצביע על שאר האסמבליס בהן תלויה האסמבלי שלנו, וארבעת נתוני ה-Strong name. 

 

ב. הדגמה מעשית, היתרונות של אסמבליס (אבטחה והפנייה הדדית)

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

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

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

image

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

image

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

נבנה אפליקציה חלונאית חדשה שתייצג את המחשבון שלנו:

image

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

ניכנס לתפרט Reffrences ונבחר Add reffrence:

image

נביט על רשימת ה-Assembly reffrence שלנו ונראה את רשימת האסמבליס אשר להן קשורה האסמבלי שלנו:

image

בואו נבנה את הפרוייקט ונראה שבאמת קיבלנו אסמבלי גם לאפליקציית EXE שלנו בתייקת ה-bin שלו:

image

בואו נחזור לרשימה הזאת שראינו קודם לכן. היה שם למשל את האסמבלי של System.Windows.Forms. כל אפליקציה חלונאית חייבת לכלול Assembly reffrence ל-System.Windows כדי להשתמש ב-Windows controls. כמו כן ראינו הפנייה ל-System אסמבלי, כל אסמבלי באשר היא תכלול הפנייה ל-System. גם ראינו שיש הפנייה ל-Justin_calc שבנינו קודם לכן. בואו נפנים לרגע מה זה אומר - כל אסמבלי מכילה ויודעת לאיזה אסמבליס אחרות היא קשורה.

אבני-יסוד שבנויות ומסתמכות על אבני-יסוד אחרות.


בואו נדבר קצת על אבטחה. כל אסמבלי לפני שהיא רצה מc:\Inetpub\WinCalc\AssemblyInfo.csבקשת הרשאות ממערכת הפעלה ומהפריימוורק על אותו מחשב. ההרשאות האלו מגוונות וכוללות למשל: הרשאה לצייר על המסך, הרשאה להדפיס למסך, הרשאה לכוננים, הרשאה לגשת לאינטרנט, הרשאה לפתוח קישורים למסדי נתונים וכך הלאה. כל ההרשאות הללו יושבות ב-System.Security ודורשות מאמר בפני עצמו. האסמבלי שהיא נטענת לזכרון מבצעת שלושה בקשות: מינימום הרשאות כדי לרוץ, הרשאות אופציונליות כדי לרוץ והרשאות שאסור לאסמבלי לקבל.

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

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

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

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

נפתח את קובץ ה-AssemblyInfo.cs שלנו ושם אנו יכולים לבקש לאסמבלי גישות מינימליות ואופציונליות וגם לבקש שלאסמבלי שלנו יסרבו גישה. בואו נגדיר את גישת הרשת כדרישה מינימלית:

// AssemblyInfo.cs
using System.Security;
using System.Security.Permissions;

//...
[assembly: PermissionSet(
    SecurityAction.RequestMinimum,
    Name="LocalIntranet")]
//...

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

// AssemblyInfo.cs
using System.Security;
using System.Security.Permissions;

//...
[assembly: PermissionSet(
    SecurityAction.RequestRefused,
    Name="LocalIntranet")]
//...

בנושא הגדרת אבטחה לאסמבלי אפשר לקחת שתי גישות:

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

 

ג. מהן Strong named assemblies וכיצד הופכים אסמבלי רגילה ל-Strong named

הזכרנו בחלק א' שכל מניפסט מכיל את שם האסמבלי, גירסת האסמבלי, התרבות אליה היא קשורה והמפתח שלה. אלו הם ארבעת הפרטים המזהים של האסמבלי. (ישנו חלק חמישי שנקרא Digital signature, ולא נעסוק בו)

שם האמסבלי נקבע בקובץ ה-AssemblyInfo.cs ואם לא קבענו אותו הוא יהיה ריק.

// AssemblyInfo.cs
[assembly: AssemblyTitle("Justin_Calc")]
// or for an empty name:
[assembly: AssemblyTitle("")]

גירסת האמסבלי רשומה בקובץ ה-AssemblyInfo.cs ואם לא קבענו אותו הוא יהיה ספרור רץ כך שכל בנייה שונה של האמסבלי תקבל מספר גירסה שונה.

// AssemblyInfo.cs
[assembly: AssemblyVersion("1.2.3.100")]

// or the default value that gives each build a unique version:
[assembly: AssemblyVersion("1.0.*")]

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

// AssemblyInfo.cs
[assembly: AssemblyCulture("He-Il")]       

// or for the invarient culture:
[assembly: AssemblyCulture("")]

המפתח הוא למעשה החלק שהופך את האמסבלי ל-Strong name assembly. מדובר למעשה בקובץ עם סיומת snk שאנו נפנה אליו מתוך ה-assembly. הקובץ הזה יכיל מפתח ציבורי ומפתח פרטי בשביל זיהוי האמסבלי ע"י אסבמליס אחרות. נפתח את ה-Visual studio 2003 command prompert (מסך שחור דמוי DOS) ונכתוב:

sn.exe -k c:\myTestKeyFile.snk

מה שנקבל הוא שבכונן C יווצר קובץ בשם myTestKeyFile.snk והוא מכיל זוג מפתחות לאפליקציה שלנו: אחד ציבורי ואחד פרטי. נעתיק את הקובץ מכונן C לתוך תיקיית הפרוייקט שלנו, לאותה תיקייה בה יושב קובץ ה-AssemblyInfo.cs שלנו. נערוך את ה-AssemblyInfo.cs כך שהוא יצביע על המפתח:

[assembly:AssemblyKeyFileAttribute("myTestKeyFile.snk")]

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

אז למה זה טוב? במילה אחת (או שלוש), זיהוי אחד ויחיד. יצרנו עכשיו אסמבלי שלה ולה בלבד יש את התכונות הללו. זיהוי וודאי.

קיימת תיקייה שנקראת Global Assembly Cache (או בקיצור: GAC). הסבר מקיף על התקייה הזאת היא מחוץ לתחום המאמר הזה, אבל נסקור בהקשרנו מה חשיבותה. בתיקייה זו ניתן לפרוס\להתקין Strong-named assemblies. ברגע שנתנו את ארבעת התכונות שהזכרנו לעיל (וליתר הדיוק, ברגע שנתנו לאסמבלי קובץ מפתח) היא נחשבת Strong named assembly. למה צריך תיקייה מיוחדת? סיבה ראשונה היא שהפריימוורק דבר ראשון ניגשת לשם לחפש אסמבליס מה שמקל מאוד על פריסת מערכות (נרחיב על כך בהמשך), סיבה שנייה היא כדי לנהל גרסאות.

אם היום נבנה את גירסה 1.0 של המנוע של המחשבון שלנו, ומחר נבנה את גירסה 1.1, מבחינת מערכת ההפעלה (עליה יושבים הקבצים) אין ביניהם שום הבדל. שניהם נקראים Justin_Calc.dll. בתיקייה מסויימת יכול לשבת רק עותק אחד של אותה אסמבלי. לעומת זאת, ב-GAC יכולות לשבת מספר גרסאות של אותה אסמבלי.

בניתי אפליקציית web שעובדת על גירסה 1.0 של מנוע המחשבון ומאחר והוא כזה טוב קיבלתי ישר הצעות לחשוף את המנוע בתור Webservice לעולם מחברות שמוכנות לשלם. אבל צריך לשנות כמה מתודות, כמה מהחתימות הקיימות של המתודות, להוסיף כמה מתודות, קיצר - שינוי טוטאלי באסמבלי. נשנה את הגירסהשל המנוע שלנו ל-2.0. ב-GAC שלנו יכולות לשבת גם גירסה 1.0 וגם גירסה 2.0, וכל אפליקציה יודעת עם איזו גירסה היא אמורה לעבוד. האפליקציית ASP.net שלי יודעת לעבוד עם גירסה 1.0 וה-Webservice יודע לעבוד עם גירסה 2.0.

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

ד. איפה וכיצד מחפשת האסמבלי את האסמבליס בהן היא תלויה.

ברגע שהגדרנו שאסמבלי תלויה באסמבליס אחרות, נפתחת לנו בעיה חדשה לחלוטין: אם פתחנו אפליקיית דוט נט למשל עם סיומת EXE שראינו שהיא אסמבלי, איפה היא תחפש את ה-DLLים שהיא תלויה בהם? (כנ"ל כמובן יהיה תקף על אפליקציןת ASP.net ו-Webserviceים)

  1. דבר ראשון שהפריימוורק עושה זה לברר את הפרטים של האמסבלי אותה היא מחפשת. שם האמסבלי, הגירסה של האסמבלי, התרבות שלה והמפתח הציבורי שלה (כפי שהוא מופיע בקובץ המפתח שהוספנו לאסמבלי שלנו). אפשר לראות דוגמה לקישור ל-System.Drawing מתוך אפליקציית ה-WinCalc שלנו כפי שהוא נראה בReflector:
    image
  2. הפריימוורק תחפש בקבצי קונפיגיורציה למיניהם הוראות מיוחדות. קבצי קונפיגיורציה כאלו יכולים לבוא בסמוך לאסמבלי שנטענת לזכרון, יכולים להיות ברמת המכונה וניתן גם שהם יהיו ברמת הארגון. בקבצים כאלו נקבעת מדיניות בנושא שימוש באסמבליס מסויימות, אך אינם הכרחיים לעבודה עימן ולכן לא נסקור אותם במאמר זה.
  3. הפריימוורק תבדוק אם האסמבלי שהיא מחפשת כבר נטענה לאחרונה לזכרון ואם כן, בזה מסתיים החיפוש.
  4. אם הפריימוורק מחפשת אסמבלי שהיא Strong name היא עכשיו תחפש בספריית ה-GAC.
  5. אם הפריימוורק לא מצאה עד עכשיו את האסמבלי היא תתחיל "לחפור" בתוך התיקייה של האסמבלי שרוצים לטעון לזכרון. אם לאפליקציה שלנו קוראים WinCalc (שיושבת ישירות על כונן C) והאסמבלי שאנו מחפשים היא בעלת השם Justin_Calc בתרבות He-IL אלו יהיה הנתיבים בהם תחפש הפיימוורק: (לפי הסדר)

C:\WinCalc\Justin_Calc.dll
C:\WinCalc\Justin_Calc\Justin_Calc.dll
C:\WinCalc\Justin_Calc.exe
C:\WinCalc\Justin_Calc\Justin_Calc.exe
C:\WinCalc\He-IL\Justin_Calc.dll
C:\WinCalc\He-IL\Justin_Calc\Justin_Calc.dll
C:\WinCalc\He-IL\Justin_Calc.exe
C:\WinCalc\He-IL\Justin_Calc\Justin_Calc.exe

ה. יתרונות ה-Strong name לאסמבליס

  1. יחודיות שם האסמבלי בעזרת המפתח - אי-אפשר ליצור בטעות שתי אסמבליס עם מפתח פרטי וציבורי זהים. באמצעות היחודיות הזו אפשר לבצע זיהוי וודאי שהאסמבלי אליה קשרנו את האסמבלי שלנו היא אכן האסמבלי שאנו צריכים.
  2. פריסה ב-GAC - אסמבלי שאינה Strong-name לא ניתן לפרוס ב-GAC. פריסה ב-GAC מבטיחה שללא חשיבות היכן נתקין על אותה מכונה את האפליקציה שלנו היא תמיד תמצא את האסמבליס בהן היא משתמשת.  אנחנו לא צריכים להתחיל להתעסק עם איפה פרסנו את האסמבליס שלנו, היות וברגע שפרסנו אותן ל-GAC הפריימוורק תמיד תמצא אותן.
  3. ניהול גרסאות נכון בפריסת מערכות -  כאשר יש לנו גירסה אחת ויחידהשל אסמבלי אליה אנו רוצים ליצור קישור אז הכל בסדר. אבל ברגע שיש לנו שתי גרסאות (או מאות גרסאות!) כבר אי-אפשר סתם להעתיק אותה לתוך תיקיית הפרוייקט. אנחנו נתחיל להסתבך עם "רגע, איזה גרסה יש לנו כרגע מותקנת שם?" ו-"לעזאזל, אני עובד עם הגירסה הלא נכונה בזמן פיתוח" ועוד נענה ללקוח "זה בסדר, זה בסדר, הכל פועל פשוט יש לך גירסה לא נכונה של המנוע הזה והזה".
  4. התייחסות להבדלי גירסאות - אם  נבחר אמסבלי לאסמבלי אחרת, נוכל להחליף את האסמבלי שמחברים בטעות בגירסה אחרת שלה ע"י העתקת הגירסה הלא-נכונה לתוך ספריית האפליקציה. לעומת זאת, כאשר אנו עובדים עם Strong name האפליקציה שמחפשת את האסמבליס הקושרות אליה מחפשת גירסה ספציפית. באסמבליס שהן לא Strong name אין חשיבות מבחינת הקישוריות בין אסמבליס לאיזה גירסה קבענו.
מבחינה תפיסתית, כאשר אנו רוצים ליצור אסמבלי שהיא משותפת בין שתיים (או יותר) אפליקציות שלנו ו\או עשויה לצאת במספר גרסאות, תמיד לפני פריסת המערכת ניצור אותה כ-Strong name ובכך נמנע מאין ספור מרעין-בישין שתמיד ניתקל בהם אם לא נעשה כך.

 

לסיכום

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

 

בהצלחה לכולכם,

ג'סטין-יוסף אנג'ל 

 

שינויים:
1. 15.11.05 -תוקנה ההתייחסות בדוגמה של המחשבון מ-Refrence מ-System.Windows ל-Refrence ל-System.Windows.Forms. תודה לאורן ואריק שציינו את זה.

שאלות מראיונות עבודה בדוט נט - האם יש לך מה שדרוש?

שלום לכולם,

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

 

1. הפניות לרכיבים מסוג using או reffrences, באם לא נעשה בהן שימוש, יראו בקוד ה-IL?

לא. במהלך קומפילציה כל הפניה מסוג using או reffrences לא תירשם בקוד ה-IL אלא אם כן היא בשימוש כלשהו בתוך הקוד. קרי, הפניה לרכיב חיצוני או הפנייה ל-namespace שלא בשימוש לא יראו בקוד הסופי.

using System; // Will be included in the MSIL
using System.Data; // Will not even show up in the MSIL
 
class myClass
{
  public static void Main()
  {
    Console.Writeline(“Hello world“);
  }
}

 

2. מה ההבדל בין readonly ל-constant?

יותר קל לעמוד על הדימיון ביניהם: הם קבועים בזמן ריצת התוכנית. ההבדל המהותי הוא כזה: הערך שנותנים ל-constant נכתב בקוד ובזמן קימפול האפליקציה פשוט מועתק לכל מקום בו הוא בשימוש, כלומר שבזמן הריצה בכלל לא קיים שום משתנה מסוג constant. על הצד השני readonly אכן קיים בזמן הריצה כמשתנה וצריך לאתחל אותו או בשורת הצהרת המשתנה או בתוך ה-constructor של המחלקה. עוד הבדל חשוב הוא ש-constant לא יכול להיות אובייקט אלא חייב להיות משהו פשוט (int, string, bool וכיו”ב)

 

3. מה זה boxing ומה זה unboxing?

באחד מהמאמרים הקודמים שלי (”Struct ו-Class -ההבדלים והכוח הטמון בשוני ביניהם”) דיברתי על ההבדלים בין Value types ל-reffrence type ומה זה Stack ו-Heap. בקצרה, כל מחלקה בדוט נט יורשת בהכרח מ-System.Object ובהקצאת זכרון תשב פיזית על ה-Heap כאשר ההפנייה למיקום על ה-Heap ישב ב-Stack. ישנה קבוצה קטנה של מחלקות שיורשות מ-System.ValueType (למשל int, bool, enums, structs וכיו”ב) שהם יושבים פיזית על ה-Stack. פעולת ה-boxing היא לקיחת ValueType והעתקו ל-Heap עם יצירת הפנייה ל-heap שיושבת על ה-stack. הפעולה unboxing היא ההפוכה והיא לקיחת ValueType שעשו לו boxing ויצירת אובייקט זהה ב-stack.

 

4. האם ניתן לתת הרשאות public/private/internal שונות ל-get של Property ול-Set של property?

לא. הגישה ל-get ול-set של property הם בעלי אותה רמת גישה בדיוק (שנלקחת מרמת הגישה כפי שנקבעה לכל ה-Property).

 

5. מהי הרשאת internal?

בדומה להרשאות גישה של public/private קיימת הרשאה בשם internal. הראשת internal קובעת כי המשאב הזה יהיה זמין רק בתוך ה-assembly שבה נכתבה המחלקה. למשל, אם יצרנו שכבה אחת במערכת שרצינו להגביל לה גישה למתודה\מחלקה\Property מסויימת לשימוש פנימי בתוך אותה assembly נשתמש בהרשאת הגישה internal.

 

6. מה ידפיס הקוד הבא?

public void myMethod()
{
  try
  {
    Console.Writeline(“Hello“);
    return;
  }
 
  catch
  {
    Console.Writeline(“ my “);
  }
 
  finally
  {
    Console.Writeline(“ world“);
   }
}

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

 

7. מהם ה“דורות“ של ה-Garbage collector?

ה-Garbage collector מבוסס על העקרון הבא: כל אובייקט חדש שנוצר מתווסף למה שנקרא “דור 0“. מספר הדור אומר כמה “איסופים“ של ה-Garbage collector הוא “שרד“. “איסוף“ מתייחס לכאשר ה-garbage collector מחליט שצריך לפנות זכרון ובודק אילו אובייקטים יש להשאיר ואיזה אפשר לזרוק. אם ה-garbage collector קובע שהאובייקט צריך להישאר הוא יעביר אותו ל“דור 1“ ובאיסוף הבא ל“דור 2“. אין דורות מעבר לדור 2.

 

8. האם ניתן להכריח את ה-Garbage collector לאסוף אובייקטים כדי לפנות מקום בזכרון? והאם כדי?

כן, אפשר. המתודה הסטטית GC.Collect מאפשרת להריץ את ה-garbage collector על “דור” מסויים או כל ה”דורות” בזכרון. לעומת זאת, העובדה שהפונקציה קיימת שם לא אומר שזה בהכרח רעיון טוב. מלבד מצבי קצה, עדיף להימנע מלקרוא ל-GC.Collect היות ורק עצם הזמן שה-Garbage collector משקיע בלברר איזה אובייקטים להשמיד ואיזה לא להשמיד (ולהעביר דור אחד הלאה) לוקח הרבה יותר משאבים וזמן מאשר אלו שאנו רוצים לחסוך. תנו ל-Garbage collector לנהל לעצמו את הזכרון.

 

9. האם אפשר לכתוב Desctructor למחלקות? והאם כדי? (Destructor היא פונקציה שמתבצעת בעת האיסוף של ה-garbage collector)

כן, אפשר.

class myClass
{
public myClass() {} // Constructor
public ~myClass() {} // Destructor
}

על הצד השני - לא עושים כזה דבר (אלמלא במצבי קצה). כאשר ה-Destructor רץ זה אומר שה-Garbage collector הגיע למסקנה ש- “טוב, אתה הולך למות כדי לפנות מקום! תמות! תמות! תמות!“, אבל אם יש Destructor אז המחלקה אומרת “יש לי עוד פונקציה להריץ!”. ומה שקורה בסוף הוא שבמקום שה-Garbage collector יהרוג את המחלקה, היא חוזרת לזכרון כדי להריץ את ה-Destructor שלה וה-Garbage collector יחכה לאיסוף הבא כדי להרוג אותה. במקום לחסוך זכרון או לעזור במשהו, רק דפקנו את ה-Garbage collection.

מתי כן נכתוב Destructor? כאשר כותבים “ישומיי תשתית”, למשל אם כותבים מחדש מחלקה שמבצעת חיבור לקבצים ורוצים להיות בטוחים שהמשאב נסגר, אם כותבים מחלקה שמבצעת חיבור למסד נתונים ורוצים להיות בטוחים שהמשאב נסגר, אם כותבים Wrapper לקוד שנכתב ב-++c וכך הלאה. אלא אם כן יש באמת צורך אמיתי וברור ל-Destructor, לא כותבים אותו.  

 

10. איך משתמשים בפונקציה מ-DLL חיצוני?

משתמשים ב-namespace של System.Runtime.InteropServices, ב-Attribute שנקראת DLLImport המכילה את שם ה-DLL, כותבים פונקציה עם קידומת static extern לשם הפונקציה והחתימה שלה ב-DLL ומתפללים. למשל:

using System.Runtime.InteropServices;
class myImport
{
[DllImport("user32.dll")]

public static extern int MessageBoxA(int h, string m, string c, int type);

public static int Main()
{
  return MessageBoxA(0, "Hello World!", "myHello", 0);
}
}

 

11. כתבנו [DLLImport], הצהרנו על פונקציה וכאשר אנו קוראים לה, שום דבר לא קורה, מדוע?

חובה לציין לפני חתימת הפונקציה מה-DLL שהיא מסוג static extern.

 

12. מהי קידומת unsafe לבלוקי קוד \ מתודות?

קידומת זו מאפשרת לנו לכתוב בלוקי קוד עם פוינטרים (חברינו האבודים מ-++c). בנוסף לקידומת זו, צריך לשנות בתוך ה-Project properties, בתוך Configuration properties את התכונה Allow unsafe code blocks ל-true. נראה קוד לדוגמה:

unsafe
{
  int* myPointer; // Reffrence to new empty pointer
  int myIntValue = 9; // new int value
 
  // Set pointer to point to the new int memory location
  myPointer = &myIntValue; 
 
  // print value of pointer: will print “myPointer is 9“
  Console.WriteLine(”myPointer is: “ + *myPointer);
}


13. האם חובה לכתוב על כל פונקציה extern שהיא מסוג Unsafe?

לא. ואלא אם כן זה באמת נדרש (עבודה מסוכנת עם pointerים) בתוך ה-DLL, עדיף להימנע מלעשות את זה.

 

14. כאשר אנו משתמשים ב-int בסביבת הפיתוח של #C, באיזה טיפוס נתונים אנו משתמשים באמת?

כאשר אנו משתמשים ב-int אנו משתמשים באמת ב-System.Int32.

 

15. אילו סוגים של intים קיימים?

Int16 - שהערך המינימלי שלו -32767 והערך המקסימלי שלו 32767.

Int32 - שהערך המינימלי שלו 2,147,483,648- והערך המקסימלי 2,147,483,648.

Int64 - שהערך המינימלי שלו 9,223,372,036,854,775,808- והערך המקסימלי שלו 9,223,372,036,854,775,808.

IntPtr - מחלקה זו היא תלויה בחומרת המחשב עליה היא רצה. במחשב 32bit היא Int32 ובמחשב 64bit היא Int64. המחלקה שימושית מאוד שצריך להעביר פוינטרים למספרים לכל מיני Reffrences שדורשות אותן (למשל המקרה הנפוץ הוא DLLImport למתודה שמקבלת פוינטר לספר).

 

16. מה זאת CLScompliant Attribute?

ה-Attribute הזאתי באה ומצהירה על המחלקה\אסמבלי\... עליה היא רשומה את הדבר הבא “אפשר לתרגם אותי לשפות אחרות בדוט נט”. אם נוסיף את התכונה הזאת למשל לכל האסמבלי שלנו (דוגמה בהמשך) הקומפיילר יבדוק:
* האם שמות המתודות שלנו שונות בלי קשר ל-casing (אחרת יכול להיווצר מצב של שתי מתודות עם אותו שם עם הבדל של casing ולמשל ב-VB.net זה בלתי אפשרי).
* כל מתודה\Property שמוצהרת כ-public אסור שתתחיל במקף תחתון.
* אין Opearator overloading, כלומר לא
התחלנו להקצות התנהגות מיוחדת לכל האופרטורים שלנו.
* הוא בודק שאין חתימות זהות לפונקציות ומתעלם מפרמטרים מסוג ref/out.

כדי להכיל על אסמבלי למשל שהיא CLScomplaint נוסיף ב-AssesmblyInfo.cs את השורה הבאה:

[assembly: CLSCompliant(true)]

 

17. איזה סוגי Intים לא הזכרתי בתשובה בסעיף 15? (רמז: שאלה 16)

UInt16, UInt32, UInt64, UIntPtr. הערכים המקסימליים של ה-UIntים כפולים מהערכים המקסימלים של ה-Intים הרגילים. אבל, הם לא CLScompliant. כלומר, הם קיימים ב-#C, אבל הם לא קיימים בפירוט השפה של דוט נט ולכן כנראה ולא יתרגמו טוב לשפות דוט נטיות אחרות. אם נרצה לכתוב על אסמבלי שהיא CLSComplaint בזמן שאנו עובדים עם Uintים נציין שהמתודה\... שעובדת עם ה-Uintים היא לא CLScomplaint.

// In AssesmblyInfo.cs
[assembly: CLSCompliant(true)]
 
// Somewhere in the assembly: (From MSDN)
[CLSCompliant(false)]
public int SetValue(UInt32 value);

 

18. מה המקביל (מבחינת ערכי מקסימום ומינימום) של Int128 (שאינו קיים) ושל Uint64 (שאמרנו שערכיו המקסימליים והמינימלים כפולים מאלו של Int64)?

הטיפוס decimal.

 

19. מדוע ומתי עדיף לעבוד עם StringBuilder מאשר עם String?
(אם הייתי מקבל שקל כל פעם ששאלו אותי את זה...)

String הוא מה שנקרא Imuteable, הווה אומר כל פעם שנשנה לו את הערך תיווצר מחרוזת חדשה לחלוטין עם הערך השונה. אם למשל נרצה לשרשר את כל המספרים בין אחד למיליון בתוך מחרוזת ונעשה את זה במחרוזת רגילה - כל פעם בתוך הלולאה תיווצר מחרוזת חדשה לחלוטין. מיליון מחרוזת זה קצת בזבזני. במקום זה למקרים כאלו עובדים עם StringBuilder ובעיקר עם מתודות Append ו-ToString. ככה ניתן להוסיף כל פעם ל-StringBuilder או לשנות אותו בהתאם לצרכינו וליצור מחרוזת אמיתית רק כאשר אנו צריכים אותה. כלל האצבע הוא שכאשר אנו עוברים 5 חיבורים של מחרוזות - עוברים לעבוד עם StringBuilder. חשוב לציין שעם עובדים עם פחות מ-5 השימוש ב-StringBuilder על פני String הופך להיות בזבזני מאוד במשאבי מערכת.

 

20. מה ההבדל בין System.Array.CopyTo ל-System.Array.Clone?

CopyTo מבצע Deep copy, כלומר יוצר מופע חדש לחלוטין ונותן לנו הפנייה אליו. Clone מבצע Shallow Copy, כלומר CopyTo יוצר הפנייה נוספת למופע הקיים. כלומר, ב-Clone אנו נעבוד על אותו מערך כל הזמן, כאשר ב-CopyTo נעבוד על מערכים שונים. 

 

21. קיים מערך מסוג  System.Array (או כל טיפוס שיורש ממנו) כיצד ניתן לסדר אותו בסדר יורד?

נשתמש במתודת ()Array.Sort ואז במתודת ()Array.Reverse. ככה נסדר את המערך בסדר עולה ונהפוך את הסדר כך שנקבל סדר יורד.

 

22. מהם שלושת סוגי ההערות בדוט נט ומה ההבדל העקרוני ביניהן?

סלאש כפול (\\), סלאש וכוכבית (\*) עם סוגר של כוכבית וסלאש (*\) וסלאש משולש (\\\). שני הראשונים משמשים להערות בגוף הקוד, כאשר השלישי משמש לתיעוד הערות XML על מתודות\Properties\מחלקות\אסמבליס\....

 

23. כיצד ניתן לקבל את כל הערות ה-XML בגוף הקוד בתוך קובץ דוקומטציה מאורגן? ובפרט, מתוך ה-Command line?

בתוך Visual Studio בתפריט Tools קיים כפתור Generate XML documention, בלחיצה עליו כל הערות ה-XML של ה-Solution נאספות לקובץ HTML מאורגן ומעוצב היטב. אפשרות נוספת היא כאשר מבצעים קימפול מה-Command Line דרך פקודת csc.exe נוסיף doc/ וגם אז יווצר קובץ תיעוד לכל ה-XML מה-Solution.

 

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

למסד הנתונים סיקוול קיים DataProvider מיוחד ומותאם לו - System.Data.SqlClient. הוא המהיר מבין האפשרויות הבאות, אך דורש לרכוש רישיון שרת סיקוול מתאים ממיקרוסופט. בנוסף קיים OleDbClient שמאפשר גישה ג'נארית לכל מסד נתונים מתוך דוט נט. הוא חינם, אבל הוא איטי לפחות פי שתיים מה-DataProviderים היעודיים. בנוסף קיים Odbc.Net שגם הוא ג'נארי (ולמעשה חלק מ-OleDb) אבל מאפשר גישה יותר מיושנת והרבה יותר ג'נארית למסדי נתונים. הבעיה עם Odbc.Net היא שהוא איטי באופן ניכר מ-OleDbClient. היתרון שלו (כפי שנאמר), היא שהוא תומך באופן יותר ג'נארי ולכן יכול לתמוך גם במסדי נתונים מאוד מאוד ישנים.

 

25. מה ההבדל בין Overloading ל-Overriding?
(וברצינות, ראבאק, מי חשב על השאלה הזאת? איזה עובד מתוסכל מ-HR שחשב שהמושגים נשמעים ממש דומה וכולם מתבלבלים איתם? השאלה הכי מעצבנת ביקום.)

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

class myInheritedClass
{
public myInheritedClass() {}
 
virtual public void myVirtualMethod()
  {
    Console.Writeline(“I am virtual“);
  }
}

class myInhertingClass :myInheritedClass
{
  public myInhertingClass() {}
 
  override public void myVirtualMethod()
  {
    Console.Writeline(“I am override!“);
  }
}
class myMain
{
  public static void Main()
  {
    myInheritedClass a = new myInheritedClass();
    a.myVirtualMethod(); // Will print out “I am virtual!“
 
 
   myInhertingClass b = new myInhertingClass();
    b.myVirtualMethod(); // Will print out “I am override!“
 
    a = b;
    a.myVirtualMethod(); // Will print out “I am override!“
  }
}

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

 

26. האם ניתן לבצע הורשה מרובה בדוט נט?

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

 

27. מה ההבדל בין הצהרות switch ב-++C ל-#C?
(
שיהיה ברור דבר אחד, אני בקושי יודע איך לכתוב hello world ב-++C, אבל בשביל ראיונות עבודה למדתי לענות על השאלה הזאת.)

ב-++C ניתן ליפול בין הצהרות שונות של case, וב-#C זה בלתי אפשרי.

switch(myInt)
{
case 0: // fall through to 2
   Console.Writeline(“myInt Equels 0“);
case 1: // fall through to 2
case 2:
   Console.Writeline(“myInt Equels 2“);
break;
}

הקוד למעלה ירוץ ב-++C, אך יזרוק שגיאת קומפיילר ב-#C. זאת היות וכל ענף (case) בהצהרת switch מחוייב להכיל break. כלומר, לא ניתן להריץ קוד כלשהו בתוך case בלי לשים בסופו הצהרת break. אז מה כן אפשר לעשות? כאפשרות ראשונה קיים הפתרון של להשתמש ב-goto:

switch(myInt)
{
case 0: // fall through to 2
   Console.Writeline(“myInt Equels 0“);
   goto case 2;
   break;
case 1: // fall through to 2
   goto case 2;
   break;
case 2:
   Console.Writeline(“myInt Equels 2“);
break;
}

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

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

class myClass
{

  // Delegate to hold functions to be executed (param curInt)
  private delegate void mySwitchDelegate(int curInt);
 
  // Static method for case1
  static private void DoCase1(int curInt)
  {
   Console.WriteLine("Case 1 execution!");
  }
 
  // Static method for case2
  static private void DoCase2(int curInt)
  {
   Console.WriteLine("Case 2 execution!");
  }

  static void Main(string[] args)
  {
 
   int myInt = 1;
 
   // Empty Delegate
   mySwitchDelegate myDelegate = null;
 
   switch(myInt)
   {
    case 0: // fall through to 2
     // Add Execution of DoCase2 to Delegate
     myDelegate +=  new mySwitchDelegate(DoCase2);
     break;
    case 1: // Do case 1
     // Add Execution of DoCase1 to Delegate
     myDelegate+=  new mySwitchDelegate(DoCase1);
     break;
    case 2: // Do case 2
     // Add Execution of DoCase2 to Delegate
     myDelegate +=  new mySwitchDelegate(DoCase2);
     break;
   }
 
   // Execute myDelegate with current myInt
   myDelegate(myInt);

   Console.ReadLine();
  }
}

אם נבחר myInt שהוא 1 יודפס (פעם אחת) Case 1 execution ואם נבחר myInt שהוא 2 יודפס (פעם אחת) Case 2 execution. בשיטה הזאתי הכל הרבה יותר נקי ומסודר ואפשר לפתח עץ לוגי שלם עם חזרות בתוך הצהרת ה-switch.

 

 

עוד שאלות נפוצות הן פשוטות ובסיסיות, למשל:

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

- האם אפשר לבצע הורשה מרובה מממשקים?
כן, אין סיבה שלא.

- כיצד יוצרים מערך מסוג זה או זה?

- מה הם Jagged arrays?
מערך של מערכים בגדלים שונים.

- מה ההבדל בין Struct ל-Class?
Struct ו-Class -ההבדלים והכוח הטמון בשוני ביניהם

- שאלות על מילת מפתח sealed, כגון: כיצד ניתן למנוע ירושה ממחלקה? כיצד ניתן למנוע דריסה של מתודה?

- איזה חתימות אפשריות יש לפונקצית Main?
() public static void Main
() public static int Main
public static void Main( string[] args )
public static int Main(string[] args )

- מה ההבדל בין Delegate ל-event?
Delegate אפשר לרוקן ממתודות, ו-event אי-אפשר לרוקן ממתודות. event הוא הדרך לחשוף delegateים ב-OOP.

- מהי אסמבלי?

- שאלות על Global assembly cache, למשל: מה ההבדל בין Shared assembly ל-private assmebly? מהו strongname, כיצד מייצרים אחד ומה הוא מכיל?
Shared assembly היא assembly שנמצאת ב-Global assembly cache (או GAC בקיצור), Private assembly בשימוש באפליקציה אחת בלבד. Strong name הוא שם שמזהה אפליקציה באופן ספציפי ומכיל גירסה, תרבות, שם האסמבלי ומפתח זיהוי יחודי. כל אסמבלי ב-Global assembly cache צריכה Strong name.

 

לסיכום

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

אם יש לכם שאלות נוספות, שאלות\הערות\תגובות על התשובות שלי, או כל דבר אחר - תרגישו חופשיים להשאיר תגובה.

ברכות,

ג'סטין-יוסף אנג'ל

Visual Studio .Net 2005 - מצגת Webcast שלי בנושא Themes

שלום לכולם,

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

עדכון: בזכות עזרתו של MVP אסף שלי במקום קובץ AVI של 125 מגה ניתן כעת לצפות במצגת כקובץ Streaming בגודל 25 מגה.

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

כחלק מהאודישן הזה הייתי צריך לדבר במשך עשר דקות על כל נושא לבחירתי.
הנושא עליו דיברתי היה Themes ב-Visual studio .Net 2005 ו-ASP.net 2.0.

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

אז לפניכם: Visual Studio .Net 2005 Themes Webcast

image
(רזולוציית מסך מומלצת 768 * 1024)

קבצי המקור של הפרוייקט לדוגמה המשתמש ב-Themes זמינים להורדה.

המצגת בנושא Themes ב-ASP.net 2.0 שהשתמשתי בה גם זמינה להורדה.

אם אתה מקבלים בעיית Codec בנגן ווידאו שלכם צריך להוריד את ה-Codec של TechSmith (גודל: 170kb).

 

אשמח לקבל תגובות,

ג'סטין-יוסף אנג'ל

Full Guide to System.Net.Mail

שלום לכולם,

בעתיד הקרוב אני ניגש למבחן העדכון של MCSD ל-MCPD ואחד מהנושאים שם זה שליחת דוא"ל דרך דוט נט. אז בואו ונדבר על System.Net.Mail.

System.Net.Mail הוא הגירסה בדוט נט 2.0 של System.Web.Mail. כן כן, שמתם לב להבדל - העבירו את זה Namespace משני.

 

חלק א': שרת ה-SMTP ב-IIS (או: "The little email who could")

בואו נתחיל מהתחלה. בכל שרת אינטרנט של מיקרוסופט מותקן IIS שזה ה-Internet information services. שמתם לב שהדגשתי את ה-s ב-services? כן, ה-IIS המיקרוסופטי מכיל מספר שירותים אפשריים ולא רק את היכולת להגיש דפי אינטרנט. שלושת השירותים העיקריים שה-IIS מציע הם: הגשת דפי אינטרנט, גישת FTP ושרת SMTP. כולנו מכירים את יכולת הגשת דפי אינטרנט וכולנו פחות או יותר יודעים מה זה FTP ("נו, אתה יודע, זה כמו העתקת קבצים רגילה רק איטי ומתסכל"). אבל מה זה ה-SMTP הזה שהזכרתי?

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

בואו נביט ונראה שבאמת קיים כזה דבר. נפתח את IIS manager (פותחים Start --> Run --> Intermgr) ונביט מה אנחנו רואים שם:

image

כמו שאמרנו ה-IIS מציע לנו שלושה שרתים אפשריים: Web, Ftp, Smtp. בואו נביט על c:\inetpub

image

בתוך תיקיית ה-inetpub שהיא ברירת המחדל ל-IIS אפשר לראות שלושה תיקיות חשובות: wwwroot, ftproot, mailroot. את wwwroot כולנו מכירים מ-ASP.Net והיא הספרייה שבה אנו מפתחים ופורשים אפליקציות ASP.Net. כלומר, שם יושבים הקבצים בשביל השירות שדואג להגיש דפי אינטרנט. בצורה דומה, ה-ftproot היא הספרייה הראשית אליה ניגשים כאשר ניגשים לשירות ה-ftp של ה-IIS. לעומת הפשטות של ספריות ה-wwwroot וה-ftproot ספריית ה-mailroot קצת יותר מורכבת. בואו נביט עליה:

image

יש שלוש תיקיות עיקריות ב-mailroot:

  1. Pickup - כאשר פריט דואר נשלח לראשונה הוא מגיע לכאן ומחכה לניסיון הראשון לשלוח אותו.
  2. Queue - אם לאחר השליחה הראשונית יש צורך להמתין מאיזהשהי סיבה כאן ימתין הדוא"ל.
  3. Badmail - גן-עדן לדוא"ל שמשהו השתבש בצורה דרסטית בשליחה שלו.

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

 

חלק ב': שליחת דוא"ל ראשון (או: "אם בהתחלה לא הצלחת כנראה שנכשלת")

בואו נראה קוד מאוד בסיסי לשליחת מייל

using System.Net.Mail;

MailMessage myMessage = new MailMessage("mail@JustinAngel.net","J@JustinAngel.net",
       "simple mail","hello justin, how are you today?");

SmtpClient mySmtp = new SmtpClient("127.0.0.1", 25);

mySmtp.Send(myMessage);

בקוד יצרנו MailMessgae חדשה שמקבלת כפרמטר ראשון את כתובת הדוא"ל של השולח, כפרמטר שני את כתובת הדוא"ל של מען ההודעה, נושא הדוא"ל ותוכן הדוא"ל. לאחר מכן פתחנו SmtpClient על המחשב הלוקאלי (פורט 25 הוא הפורט ברירת המחדל של ה-SMTP) ושלחנו דרכו את הדוא"ל.

מעכשיו ככה זה הולך לעבוד: MailMessage זה הדוא"ל שנשלח ו-SmtpClient שולח הודעות דוא"ל.

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

Mailbox unavailable. The server response was: 5.7.1 Unable to relay for J@justinangel.net

image

הפתרון יחסית פשוט:

Start --> Run --> intermgr --> right click smtp server --> Access --> Realy --> Add --> Ip Address: 127.0.0.1 --> Ok --> Apply

image

או קיי, טיפלנו בזה - בואו נריץ שוב.

הכל רץ חלק. ביינתים הדוא"ל שלנו הגיע לתיקיית inetpub\mailroot\Pickup ושרת ה-Smtp ניסה לשלוח אותו. ודרך הפיירוול במחשב חסמתי את פורט 25 ככה שביינתים השרת לא הצליח והדוא"ל עבר לתיקיית inetpub\wwwroot\Queue. נפתח את תיקיית ה-Queue:

image

נפתח את הקובץ הזה בפנקס רשימת וזה מה שנראה:

Received: from justin ([127.0.0.1]) by justin with Microsoft SMTPSVC(5.0.2172.1);
  Fri, 3 Feb 2006 19:31:57 +0200
mime-version: 1.0
from: J@justinAngel.net
to: mail@JustinAngel.net
date: 3 Feb 2006 19:31:57 +0200
subject: simple mail
content-type: text/plain; charset=us-ascii
content-transfer-encoding: quoted-printable
Return-Path: J@justinAngel.net
Message-ID: <JUSTINUFSXju17BKjJ000000001@justin>
X-OriginalArrivalTime: 03 Feb 2006 17:31:57.0344 (UTC) FILETIME=[BB696200:01C628E7]

hello justin, how are you today?

בקצרה, מדובר על פורמט MIME וזה הפורמט שדוא"ל עובר בעולם. אפשר לראות שנושא ההודעה שלנו שם, הכתובת מען שלנו שם, הכתובת משלוח המקורית שם ותוכן ההודעה שם. אז במקום שאנחנו נצטרך לדעת איך לעבוד עם הפורמט הזה מה שלמעשה קרה זה ש-System.Net.Mail כתב אותו בהתאם למה שביקשנו ממנו.

בואו נשכתב את הקוד שכתבנו למעלה ובמקום לעבוד עם Constructors נעבוד עם מאפיינים (גם באנגלית: Properties).


MailMessage
myMessage = new MailMessage();
myMessage.To.Add("J@justinAngel.net");
myMessage.From =  "mail@JustinAngel.net"; // will cause compliation error
myMessage.Subject = "simple mail";
myMessage.Body = "hello justin, how are you today?";

SmtpClient mySmtp = new SmtpClient();
mySmtp.Host =
"127.0.0.1";
mySmtp.Port = 25;

mySmtp.Send(myMessage);

חלק ג': חוקרים את MailMessage (או: "4,000 טריקים בתוך מאפיינים")

MailAdress - שכתוב הדוגמה

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@justinAngel.net", "Justin Angel"));

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  myMessage.Subject = "simple mail";

  myMessage.Body = "hello justin, how are you today?";

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

כמען לדוא"ל יצרנו מחלקת MailAddress חדשה שתפנה לכתובת הדוא"ל שלי J@JustinAngel.Net ותציג בתצוגה את השם שלי Justin Angel. כשולח לדוא"ל יצרנו MailAddress נוסף אבל שיכיל כתובת דוא"ל בלבד בלי שם ותיאור.

MailMessage.Form הוא מסוג מחלקת MailAddress (ולכן הדוגמה האחרונה בחלק ב' לא תתקמפל).

אם הבטתם מקרוב שמתם לב למשהו מעניין MailMessage.To הוא לא ערך בודד כמו MailMessage.From אלא אוסף של MailAddress. כלומר, לדוא"ל יש אוסף של מענים ולא מען בודד. עוד אינפורמציה מעניינת (וטיפה לא קשורה) היא שמדובר באוסף שפנימית עובד עם Generics. עכשיו, אתם כנראה חושבים "היי, זה מתחיל להזכיר לי את האי-מיילים בעבודה! אבל לאי-מיילים בעבודה יש עוד המון תכונות למשל גם יש מעני CC ומעני BCC". אז נכון, יש MailMessage.To שאלו הם המענים הרגילים, יש את MailMessage.CC שאלו הם המענים המכותבים ויש את MailMessage.BCC שאלו הם המענים המכותבים הנסתרים.

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  myMessage.Subject = "simple mail";

  myMessage.Body = "hello justin, how are you today?";

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

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

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

 

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

            myMessage.Priority = MailPriority.High;

 

  myMessage.Subject = "simple mail";

  myMessage.Body = "hello justin, how are you today?";

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

            myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "simple mail";

  myMessage.Body = "hello justin, how are you today?";

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

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

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

  myMessage.Body = "hello justin, how are you today?" + "מה המצב גבר גבר?";

  myMessage.BodyEncoding = Encoding.GetEncoding("iso-8859-8-i");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

  myMessage.Body = "hello justin, how are you today?" + "מה המצב גבר גבר?";

  myMessage.BodyEncoding = Encoding.GetEncoding("iso-8859-8-i");

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(),""));

 

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

        private static System.IO.Stream GetAttachmentStream()

        {

           return new FileStream(@"c:\myFile.txt", FileMode.Open);

        }

אפשר לראות שהוספנו להודעת הדוא"ל שלנו שני מחלקות מסוג Attachment. המחלקה Attachment מייצגת קובץ שנספח לדוא"ל ויכלה להיווצר או מנתיב מקומי לקובץ או מ-Stream שמכיל את הקובץ.

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

            myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

  myMessage.Body = "<b>" + "hello justin, how are you today?" + "מה המצב גבר גבר?" + "</b>";

            myMessage.IsBodyHtml = true;

  myMessage.BodyEncoding = Encoding.GetEncoding("iso-8859-8-i");

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(), ""));

 

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

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

כל זה טוב ויפה, אבל HTML זאת טכנולוגיה מתקדמת. ייתכן וכי יש שירותי דואר שלא תומכים בהצגת HTML גרפי (בכוונה או מחוסר יכולת). כדי לפתור את הבעיה הזאת באו ואמרו ככה - את הדוא"ל הזה אפשר להציג לפי סוג התוכן שביקשת. הכי נפוץ לראות דוא"ל בפורמט text/plain שזה טקסט פשוט ודוא"ל בפורמט text/html שזו תצוגת HTML. אבל יש עוד המון פורמטים, למשל אתמול קיבלתי דוא"ל שמציג טבלה מאוד ארוכה ומורכבת. בתצוגת HTML היה ניתן לראות את הטבלה, בתצוגת טקסט היה אפשר לראות את הטקסט "אנא פתח אוטלוק לתצוגת טבלה" ותצוגה שלישית כמסמך אקסל (!). על כל עניין הפורמטים הזה בנו Namespace שלם בשם System.Net.Mime שהוא האח הקטן והשימושי של System.Net.Mail. בתוך System.Net.Mime יש מחלקה בשם ContentType שמחזיקה סוג פורמט.

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(), ""));

 

  ContentType PlainContentType = new ContentType("text/plain");

  AlternateView PlainView = AlternateView.CreateAlternateViewFromString

    ("Hi justin, how are you?", PlainContentType);

  myMessage.AlternateViews.Add(PlainView);

 

  ContentType HtmlContentType = new ContentType("text/html");

  AlternateView HtmlView = AlternateView.CreateAlternateViewFromString

    ("<b>Hi justin, how are you?</b>", HtmlContentType);

  myMessage.AlternateViews.Add(HtmlView);

 

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

עשינו משהו מאוד פשוט - הוספנו ContentType חדש, הוספנו AlternateView לאותו סוג פורמט ואת אותו AlternateView הוספנו ל-MailMessage.AlternateViews (שזה אוסף של AlternateViews). וככה אנחנו יכולים מאוד בקלות ליצור כמה תצוגות שצריך לדוא"ל שלנו. סה"כ זה מנהג מאוד טוב להוסיף AlternateView אחד ל-HTML ו-AlternateView אחד לטקסט פשוט.

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

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(), ""));

 

            myMessage.BodyEncoding = Encoding.UTF8;

 

  ContentType PlainContentType = new ContentType("text/plain");

            AlternateView PlainView = AlternateView.CreateAlternateViewFromString

    ("אהלן ג'סטין, מה המצב גבר גבר?", PlainContentType);

  PlainView.TransferEncoding = TransferEncoding.Base64;

  myMessage.AlternateViews.Add(PlainView);

 

 

  ContentType HtmlContentType = new ContentType("text/html");

  AlternateView HtmlView = AlternateView.CreateAlternateViewFromString

              ("<b>Hi justin, how are you?</b>", HtmlContentType);

  myMessage.AlternateViews.Add(HtmlView);

 

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

AlternateView הוא למעשה סוג של Attachment לדוא"ל ולכן הוא קצת יותר מוגבל מבחינת קידוד (TransferEncoding הוא Enum יחסית מוגבל שמתייחס לקידוד בו מועבר הקובץ). זאת בזמן ש-MailMessage.BodyEncoding מתייחס לקידוד שבו תוצג ההודעה.

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

נשתמש בחלק האחרון של AlternateView - ב-LinkedResources. אוסף זה מאפשר לנו להוסיף קובץ שיהיה חלק מהמסמך ולא כקובץ כנספח חיצוני למסמך. את ה-LinkedResource ניתן ליצור (בדומה ל-Attachment) מתוך נתיב לקובץ או מתוך Stream שמכיל את הקובץ. בואו נראה דוגמה להוספת תמונה שלי בראש המסמך וכרטיס ביקור בסיומו.

          MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.From = new MailAddress("mail@JustinAngel.net");

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(), ""));

 

  myMessage.BodyEncoding = Encoding.UTF8;

 

            ContentType PlainContentType = new ContentType("text/plain");

  AlternateView PlainView = AlternateView.CreateAlternateViewFromString

    ("אהלן ג'סטין, מה המצב גבר גבר?", PlainContentType);

  PlainView.TransferEncoding = TransferEncoding.Base64;

  myMessage.AlternateViews.Add(PlainView);

 

 

  ContentType HtmlContentType = new ContentType("text/html");

  AlternateView HtmlView = AlternateView.CreateAlternateViewFromString

              ("<img src=cid:JustinPic><b>Hi justin," +

       "how are you?</b><img src=cid:JustinBusinessCard>", HtmlContentType);

       

            LinkedResource SomePic = new LinkedResource(@"c:\Justin-02-02-2006.gif");

  SomePic.ContentId = "JustinPic";

  HtmlView.LinkedResources.Add(SomePic);

 

  LinkedResource SomeOtherPic = new LinkedResource(GetBusinessCardStream());

  SomeOtherPic.ContentId = "JustinBusinessCard";

  HtmlView.LinkedResources.Add(SomeOtherPic);

 

  HtmlView.LinkedResources.Add(new LinkedResource(@"c:\JustiPic.gif"));

  myMessage.AlternateViews.Add(HtmlView);

 

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

 

        private static Stream GetBusinessCardStream()

        {

  return new FileStream(@"c:\Bizcard.jpg", FileMode.Open);

 

       }

        private static Stream GetAttachmentStream()

        {

           return new FileStream(@"c:\myFile.txt", FileMode.Open);

        }

בואו נעבור על מה שעשינו, דבר ראשון יצרנו LinkedResource שנובע מקובץ מקומי או Stream. נתנו לאותו LinkedResource.ContentId שתסמל אותו שנעבוד בתוך המסמך. בתוך המסמך, כחלק מה-HTML מוכר יצרנו תגית <img> עם ווריציה קלה - כתבנו src=cid:myLinkedResourceContentId. אגב, בכוונה נתתי לקבצים ולמחלקות LinkedResource שמות לא קשורים כדי שתראו בדיוק מאיפה מגיע ה-ContentId.

 

חלק ד': חוקרים את SmtpClient (או: "סינכרוני\א-סינכרוני, מאובטח\לא-מאובטח ומשלוח מקומי או משלוח ברשת")

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

            MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

 

 

        private static MailMessage CreateVeryComplexMailMessage()

        {

  MailMessage myMessage = new MailMessage();

 

  myMessage.To.Add(new MailAddress("J@JustinAngel.net", "Justin Angel"));

  myMessage.To.Add(new MailAddress("Ayende@ayende.com", "Oren Eini"));

  myMessage.CC.Add(new MailAddress("oren.ellenbogen@hmail.com", "Oren Ellenbogen"));

  myMessage.CC.Add(new MailAddress("royo@oshrovo.com", "Roy Osherove"));

  myMessage.Bcc.Add(new MailAddress("mikiwatts@orb-software.com", "Miki Watts"));

 

  myMessage.Priority = MailPriority.High;

  myMessage.ReplyTo = new MailAddress("do-not-reply@JustinAngel.net");

 

  myMessage.Subject = "מה שלומך?";

  myMessage.SubjectEncoding = Encoding.Unicode;

 

  myMessage.Attachments.Add(new Attachment(@"c:\myFile.txt"));

  myMessage.Attachments.Add(new Attachment(GetAttachmentStream(), ""));

 

  myMessage.BodyEncoding = Encoding.UTF8;

 

  ContentType PlainContentType = new ContentType("text/plain");

  AlternateView PlainView = AlternateView.CreateAlternateViewFromString

    ("אהלן ג'סטין, מה המצב גבר גבר?", PlainContentType);

  PlainView.TransferEncoding = TransferEncoding.Base64;

  myMessage.AlternateViews.Add(PlainView);

 

 

  ContentType HtmlContentType = new ContentType("text/html");

  AlternateView HtmlView = AlternateView.CreateAlternateViewFromString

    ("<img src=cid:JustinPic><b>Hi justin," +

       "how are you?</b><img src=cid:JustinBusinessCard>", HtmlContentType);

 

  LinkedResource SomePic = new LinkedResource(@"c:\Justin-02-02-2006.gif");

  SomePic.ContentId = "JustinPic";

  HtmlView.LinkedResources.Add(SomePic);

 

  LinkedResource SomeOtherPic = new LinkedResource(GetBusinessCardStream());

  SomeOtherPic.ContentId = "JustinBusinessCard";

  HtmlView.LinkedResources.Add(SomeOtherPic);

 

  HtmlView.LinkedResources.Add(new LinkedResource(@"c:\JustiPic.gif"));

  myMessage.AlternateViews.Add(HtmlView);

 

  return myMessage;

        }

 

        private static Stream GetBusinessCardStream()

        {

  return new FileStream(@"c:\Bizcard.jpg", FileMode.Open);

        }

 

        private static System.IO.Stream GetAttachmentStream()

        {

           return new FileStream(@"c:\myFile.txt", FileMode.Open);

        }

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

אנחנו שולחים דרך הרשת המקומית ל-IP שכתובתו 127.0.0.1 בפורט 25 דוא"ל. במקרה שלנו מדובר בשליחה לוקאלית, אבל ייתכן מאוד ונרצה לשלוח באמת דרך הרשת את הדוא"ל לשרת מרוחק (למשל לשרת Exchange). אז אנחנו נחפשים כאן לבעית אבטחה - המייילים שלנו יעברו חשופים לחלוטין דרך האל יודע איפה. מה נהוג לעשות במצב כזה? להגדיר איזהשהו פתרון של תקשורת מאובטחת. נלך על הכי פשוט וידוע - SSL. אל תטעו ותחשבו על SSL בין לקוח דפדן לשרת כמו שאנחנו רגילים מתחום ה-web, אלא כאן מדובר על תקשורת בין השרת שלנו לבין שרת דואר. בואו נראה דוגמה לאיך לפתוח בקשת מפתח SSL ציבורי משרת הדואר, קבלת המפתח הציבורי, קידוד ההודעה בעזרת ההודעה ב-128 ביט ושליחתה לשרת.

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.EnableSsl = true;

 

  mySmtp.Send(myMessage);

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

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.EnableSsl = true;

 

  mySmtp.Timeout = 10 * 1000;

 

  mySmtp.Send(myMessage);

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.EnableSsl = true;

 

  mySmtp.SendAsync(myMessage, myMessage);

עכשיו נתעכב לכמה אלפי שנייה בלבד על משלוח הדוא"ל בזמן שכל שאר הקוד שלנו ממשיך לרוץ כרגיל. שימו לב שהורדנו את SmtpClient.Timeout היות ואם אמרנו "אל תתעכב על השורה הזאת" אין טעם להוסיף "אל תתעכב על השורה הזאת יותר מ-10 שניות". אם ברצוננו לבצע פעולה לאחר שהשליחה הסתיימה (למשל להדפיס הודעה למשתמש, לרשום ביומן הפעולות או לבדוק אם עלו שגיאות במהלך הביצוע) נוכל לבצע זאת בעזרת אירוע SmtpClient.SendCompleted. האירוע יקבל את המתודות שנרצה שירוצו לאחר ששליחת הדוא"ל הסתיימה.

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.EnableSsl = true;

 

            mySmtp.SendCompleted += new SendCompletedEventHandler(mySmtp_SendCompleted);

 

  mySmtp.SendAsync(myMessage, myMessage);

 

 

        private void mySmtp_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)

        {

  Console.Write("Send completed!");

        }

עד עכשיו יצאנו מנקודת הנחה כי אנו שולחים דרך הרשת את הדוא"ל שלנו. מסתבר שיש שלוש דרכים לשלוח דוא"ל דרך שרת ה-SMTP של ה-IIS: דרך הרשת, דרך שרת ה-SMTP המקומי עם ספריית ה-Pickup שלו ולכתוב את קובץ ה-eml לתוך ספרייה כלשהי (שבתקווה היא תיקיית Pickup של שרת SMTP כלשהו).

נראה כיצד להשתמש בספריית ה-Pickup ברירת-המחדל של שרת ה-SMTP של ה-IIS שלנו.

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

            mySmtp.DeliveryMethod = SmtpDeliveryMethod.PickupDirectoryFromIis;

 

  mySmtp.Send(myMessage);

השתמנו ב-mySmtp.DeliveryMethod ואמרנו כי השליחה דרך ה-SmtpClient הזה מתבצע לתוך שרת ה-SMTP המקומי ואכן באמת נוסף לנו קובץ חדש לתוך ספריית inetpub\mailroot\Pickup.

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

            mySmtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;

  mySmtp.PickupDirectoryLocation = @"c:\Exchange2003\Pickup";

 

  mySmtp.Send(myMessage);

במקרה הזה כתבנו לתוך תיקיית ה-Pickup של שרת ה-Exchange  הלוקאלי. ניתן להשתמש באפשרות של כתיבת דוא"ל לספרייה מקומית גם בזמן פיתוח ותחזוקת אפליקציה שעובדת עם System.Net.Mail. בצורה זו נוכל לבחון את כל קבצי ה-eml שהקוד שלנו יצר.

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

MailMessage myMessage = CreateVeryComplexMailMessage();

     myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "127.0.0.1";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

            myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "254.169.20.15";

  mySmtp.Port = 25;

 

  mySmtp.Send(myMessage);

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "254.169.20.15";

  mySmtp.Port = 25;

  mySmtp.Credentials = new NetworkCredential("LanUsername", "LanPassword");

 

  mySmtp.Send(myMessage);

 

חלק ה': קובץ קונפיגיורציה (או: "חוסכים טקסט שקשור ל-SmtpClient")

נוסיף לפרוייקט שלנו קובץ קונפיגיורציה. אם נשחק קצת עם ה-intellisense נראה שנגיע למצב שיש לנו את הטקסט הבא בתוך App.config.

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

<configuration>

  <system.net>

    <mailSettings >

      <smtp>

      </smtp>

    </mailSettings>

  </system.net>

</configuration>

נעבור על כל דוגמה שהגענו אליה בסעיף הקודם ונראה כיצד ניתן להגדיר את המאפיינים של SmtpClient בתוך קובץ ה-App.config שלנו.

בדוגמה שבה תיקיית היעד שלנו הייתה תיקיית ברירת המחדל של שרת ה-SMTP של ה-IIS הקוד שלנו נראה כך:

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

            mySmtp.DeliveryMethod = SmtpDeliveryMethod.PickupDirectoryFromIis;

 

  mySmtp.Send(myMessage);

דבר ראשון, נוסיף את כתובת ה-MailMessage.From שלנו לתוך קובץ הקונפיגיורציה.

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

<configuration>

  <system.net>

    <mailSettings >

      <smtp from="mail@JustinAngel.net">

      </smtp>

    </mailSettings>

  </system.net>

</configuration>

וכעת נוכל להריץ את הקוד שלנו בלי לפרט את כתובת ה-MailMessage.From בתוך הקוד, אלא הוא ילקח כברירת מחדל מתוך קובץ הקונפיגיורציה.

  MailMessage myMessage = CreateVeryComplexMailMessage();

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.DeliveryMethod = SmtpDeliveryMethod.PickupDirectoryFromIis;

 

  mySmtp.Send(myMessage);

כל שעלינו לעשות כאן הוא לכתוב בקובץ הקונפיגיורציה שצורת משלוח דוא"ל הוא לשרת ה-SMTP של ה-IIS.

// our App.config

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

<configuration>

  <system.net>

    <mailSettings >

      <smtp from="mail@JustinAngel.net" deliveryMethod="PickupDirectoryFromIis">

      </smtp>

    </mailSettings>

  </system.net>

</configuration>

 

// our code

  MailMessage myMessage = CreateVeryComplexMailMessage();

 

  SmtpClient mySmtp = new SmtpClient();

 

  mySmtp.Send(myMessage);

הגענו למצב האופטימלי בשבילנו, כל האינפורמציה על ה-SmtpClient נלקחת מקובץ הקונפיגיורציה שלנו.

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;

  mySmtp.PickupDirectoryLocation = @"c:\Exchange2003\Pickup";

 

            mySmtp.Send(myMessage);

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

// our App.config

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

<configuration>

  <system.net>

    <mailSettings >

      <smtp from="mail@JustinAngel.net" deliveryMethod="SpecifiedPickupDirectory"

        <specifiedPickupDirectory pickupDirectoryLocation="c:\Exchange2003\Pickup"/>

      </smtp>

    </mailSettings>

  </system.net>

</configuration>

 

// our code 

  MailMessage myMessage = CreateVeryComplexMailMessage();

 

  SmtpClient mySmtp = new SmtpClient();

 

  mySmtp.Send(myMessage);

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

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

  MailMessage myMessage = CreateVeryComplexMailMessage();

  myMessage.From = new MailAddress("mail@JustinAngel.net");

 

  SmtpClient mySmtp = new SmtpClient();

  mySmtp.Host = "254.169.20.15";

  mySmtp.Port = 25;

  mySmtp.Credentials = new NetworkCredential("LanUsername", "LanPassword");

 

  mySmtp.Send(myMessage);

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

// our App.config

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

<configuration>

  <system.net>

    <mailSettings >

      <smtp from="mail@JustinAngel.net" deliveryMethod="Network">

        <network host="246.169.20.15" port="25"

       userName="LanUsername" password="LanPassword" />

      </smtp>

    </mailSettings>

  </system.net>

</configuration>

 

// our code

  MailMessage myMessage = CreateVeryComplexMailMessage();

 

  SmtpClient mySmtp = new SmtpClient();

 

  mySmtp.Send(myMessage);

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

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

 

לסיכום

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

Send electronic mail to a Simple Mail Transfer Protocol (SMTP) server for delivery from a .NET Framework application. (Refer System.Net.Mail namespace)

  • MailMessage class - הכרנו את מחלקת MailMessage. שינינו נושא, תוכן, קידוד ועדיפות.
  • MailAddress class and MailAddressCollection class - כחלק מ-MailMessage משתמשים הרבה בכתובות דוא"ל (מענים רגילים, מענים מכותבים ומענים נסתרים, , כתובת From וכתובת ReplyTo). מחלקת ה-MailAdress מאפשרת להוסיף שם תצוגה לכתובת הדוא"ל אליה אנו שולחים דוא"ל.
  • SmtpClient class - מחלקת ה-SmtpClient היא המחלקה שאחראית לשלוח MailMessage. ראינו את שלושת המצבים האפשריים לשליחת דוא"ל ואת הקונפיגיורציה האפשרית לכל מצב.
  • SmtpPermission class, and SmtpPermissionAttribute class - לא עסקנו בזאת רבות, אבל נסתפק ונגיד שזו ההרשאה לשלוח דוא"ל לאפליקציה.
  • Attachment class, AttachmentBase class, and AttachmentCollection class - ראינו כיצד להוסיף Attachment ל-MailMessage. בנוסף, צריך לזכור שגם LinkedResource, גם AlternateView וגם Attachment יורשים מאותה מחלקה - AttachmenBase היות והם באמת כאלו "נספחים להודעות דוא"ל".
  • SmtpException class, SmtpFailedReceipientException class, and SmtpFailedReceipientsException class - אלו הן החריגות האפשריות במשלוח דוא"ל. SmtpException היא החריגה הכללית. SmtpFailedReceipientExcpetion יורשת ממנו ומייצגת ששליחה אחת נכשלה (SmtpFailedReceipientsException היא החריגה שמספר שליחות נכשלו).
  • SendCompletedEventHandler delegate - זוהי המחלקה שאחראית להוספת מתודה לאירוע ה-SendCompleted לשליחה אסינכרונית.
  • LinkedResource class and LinkedResourceCollection class - מחלקת LinkedResource מייצגת נספח שאמור להיות מוצג כחלק מהתצוגה של AlternateView.
  • AlternateView class and AlternateViewCollection class - מחלקת AlternateView מאפשרת לנו ליצור תצוגות שונות לדוא"ל שלנו בהתאם לסוגי פירמוט שונים.

 

ברכות ושבת שלום,

ג'סטין-יוסף אנג'ל

ד"ר דוט נט - ביקורת על ספר מצויין חדש בעברית

שלום לכולם,

"פיתוח אפליקציות שרת-לקוח בשפת C# .Net מאת ד"ר דוט נט, ASP.Net & Xml Web Services" (מהדורה ראשונה, 2006) מאת יוסף בלאן הינו ספר חדש על דוט נט. היחוד האמיתי של הספר הזה הוא שהסופר ישראלי, הספר נכתב בישראל ופורסם במקור בעברית. בתור ספר שמקורו בתרבות הישראלית יש לו באמת גישה שפונה הרבה יותר לקהל הישראלי. המנטליות הישראלית אומרת ככה "אם אני משקיע במשהו את הזמן שלי, אני מצפה לקבל את התמורה המקסימלית. בנוסף, אני לומד הכי טוב מדוגמאות ולא מדיבורים אקדמיים".

 image

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

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

מהפרק השני עד הפרק העשירי במשך 300 עמ' ישנם 30 "פעילויות" (משימות בסדר גודל קטן אך מכובד) ואין-ספור דוגמאות קוד וצילומי מסך למכביר שמלמדים את הקורא את הבסיס ל-ASP.Net. הנושאים שמכוסים הם בכלליות: בניית טפסי ASP.Net, וולידציה ב-Webforms, גישה למסדי נתונים באמצעות ADO.Net, אפשרויות DataBinding לפקדי צד-שרת, שימוש ב-Application, Session & Cache, שימוש בקבצי Config ו-global.asax ועבודה עם אירועים בדוט נט. כל זה בצורה של ישר ולעניין - הנה המשימה, הנה מה שאנחנו יודעים, הנה הקוד והנה מה הוא אומר ומה המשמעות שלו. בצורה שמקבלים תמורה הוגנת לכל שנייה שמקדישים לספר.

מהפרק ה-11 ועד פרק 16 האחרון מתחילים לרדת מהגישה של "הנה קוד - תקרא, תבין, תשחזר" שלפי התפיסה שלי פועלת הכי טוב בהתחלה ועוברים לגישה יותר מקצוענית. הגישה בפרקים האלו היא "אתם כבר יודעים על מה מדובר ואתם כבר מכירים את הפריימוורק, אז בואו נתחיל לעבוד כמו מקצוענים". בצורה כללית בפרקים אלו הספר עוסק בהצגת שני נושאים חדשים - Object oriented programming ו-Webservices ובאמת לקרקע ולהשלים את הידע של הקורא ב-ASP.Net.

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

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

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

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

לסיכום, "פיתוח אפליקציות שרת-לקוח בשפת C# .Net מאת ד"ר דוט נט, ASP.Net & Xml Web Services" (מהדורה ראשונה, 2006) מאת יוסף בלאן הוא ספר לימוד דוט נט למתחילים מהמעלה הטובה ביותר. לא רק שמדובר על ספר מצויין, אלא גם מדובר על ספר שבה במנטליות ישראלית של עשייה להעביר לנו חומר וזה מאוד חשוב אצלי. בסה"כ הספר מקבל אצלי את הדירוג "מצויין" ו-"מומלץ מאוד למתחילים" והוא בהחלט ובלי שום תחרות ספר הלימוד הכי טוב בעברית לדוט נט. מבחינתי, לכל מי שמרגיש שהוא צריך יישור קו מקצועי או לכל מי שנכנס לתחום מדובר בספר מושלם ואני ממליץ אליו בלי עוררין.

באתר הספר http://www.drdotnet.com ניתן להוריד תוכן עניינים מלא, פרק לדוגמה, לקבל פרטים ליצירת קשר עם הסופר (פלאפון אישי!), פורום לשאלת שאלות על הספר וכיצד להזמין עותק של הספר.

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

 

ברכות ושבת שלום,

ג'סטין-יוסף אנג'ל

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

More Posts Next page »