February 2009 - Posts
גיא תמיר (הבוס שלי לשעבר בחברת
QualiSystems) שלח לי באג מעניין ב Visual Studio.
במידה ואתם מנסים להוסיף ל ComboBox אובייקטים שעושים override ל ToString, ובמצבים מסויימים מחזירים null,
האפליקצייה מתרסקת, עם OutOfMemoryException, כשבהודעת שגיאה יש: Too many items in the combo box.
לדוגמא:
1 class TestClass
2 {
3 private int i;
4
5 public TestClass(int i)
6 {
7 this.i = i;
8 }
9
10 public override string ToString()
11 {
12 if (i == 2)
13 {
14 return null;
15 }
16 return i.ToString();
17 }
18 }
בחרתי למען הדוגמא, שאם ה i == 2, אנחנו מחזירים null.
הנה השימוש בקוד.
1 private void button1_Click(object sender, EventArgs e)
2 {
3 TestClass a2 = new TestClass(20);
4 TestClass a1 = new TestClass(2);
5
6
7
8 comboBox1.Items.Add(a1);
9 comboBox1.Items.Add(a2);
10 }
שורה 9 תעיף את האפליקיציה.
גיא פתח למייקרוסופט באג, תוכלו להתעדכן
בקישור הבא.
אז תודה רבה לגיא, ותמשיך לעדכן אותנו בדברים מעניינים.
בתקופה האחרונה אני מרגיש שחזרתי לימי הבניים,
אני עובד על פרויקט web, אבל אני יכול להשתמש רק ב html ו js, מותר לי לכתוב aspx רק לשימוש כמו webService (כדי לשמור נתונים, ולייבא data)
אני משתמש במערכת תוכן של חברה מסוימת (אחד המוצרים הכי גרועים שיכולים להיות),
ומה שיש לי זה html editor, שאני יכול לכתוב בו את קוד ה html שלי.
בכל מקרה, באחד מהעמודים הייתי צריך לייצר כמה תיבות טקסט, ולחצן "שלח" כדי לשמור את המידע ב DB,
לכאורה מאוד פשוט, הקוד אמור להיראות כך:
1 <form action="server.apsx" method="post">
2 <input type="text" name="txtName" />
3 <input type="text" name="txtPhone" />
4 <input type="submit" value="click" />
5 </form>
בשורה ראשונה אני מייצר את הטופס ומגדיר שבזמן לחיצה על ה "submit", ללכת לדף server.aspx.
בדף ההוא, אני לוקח את המידע, ושומר ב DB.
6 protected void Page_Load(object sender, EventArgs e)
7 {
8 string name = Request["txtName"];
9 string phone = Request["txtPhone"];
10 }
נראה פשוט, וזה באמת עובד.
אבל....
ברגע שהמשתמש הכניס אותיות עבריות, הייתי מקבל עבור כל תו עברי, את הקוד הבא "ufffd%".
חפרתי ברשת, ראיתי שעוד אנשים נתקלו במשהו דומה, אבל לא מצאתי פיתרון.
גיליתי שאם אני משנה את ה encoding על ידי קליק ימני בעכבר (באינטרנט אקספלורר) ל UTF-8, זה עובד, אמנם זה גורם שכל הדף הופך ל UTF-8, שזה אומר שכל התווים העבריים בדף יהפכו למין מלל לא ברור, אבל את הערכים מתיבות הטקסט, אני מקבל כמו שצריך.
אז חשבתי שפיתרון אחד יהיה, שלפני שאני לוחץ על submit, אני יכול לשנות את ה encoding של הדף.
אבל בסופו של דבר מצאתי פיתרון הרבה יותר טוב, ואני מקווה שמי שייתקל בבעייה הזאת, יוכל להשתמש בפיתרון הזה.
כתבתי את פונקציית ה JS הבאה:
1 <script type="text/javascript">
2 function EscapeAllInput(frm)
3 {
4 for (var i = 0; i < frm.length; i++)
5 {
6 frm[i].value = escape(frm[i].value);
7 }
8 }
9 </script>
10 <form action="server.apsx" method="post"
11 onsubmit="EscapeAllInput(this)">
12 <input type="text" name="txtName" />
13 <input type="text" name="txtPhone" />
14 <input type="submit" value="click" />
15 </form>
הפונקצייה מקבל טופס, רצה על כל ה inputs שמוגדרים, ועושה ל value המרה ל UTF-8.
על הטופס מוגדר שב onsubmit, יש להפעיל את הפונקצייה.
בצד השרת, יש להוסיף reference ל - Microsoft.JScript.dll
ולכתוב את הקוד הבא:
16 protected void Page_Load(object sender, EventArgs e)
17 {
18 string name = GlobalObject.unescape(Request["txtName"]);
19 string phone = GlobalObject.unescape(Request["txtPhone"]);
20 }
כעת, הכל עובד מצויין.
פיני שאל אותי איך מעתיקים את הקוד לתוך PowerPoint.
ובמידה ואתם מנסים להעתיק מתוך קובץ aspx, תקראו את הפוסט
הזה.
בסופו של תהליך אתם רוצים להדביק לתוך המצגת.
מה שאתם צריכים לעשות, זה לבחור בהדבקה מיוחדת ולבחור ב html:
חבר שלי שאל אותי את השאלה הבאה:
אני מעוניין לראות הישרדות, אבל לא דרך האתר, כי הוא מלא בפירסומות, וגם אני נתקע עם ה buferring,
כמו כן אני מעוניין להוריד את זה למחשב שלי, אבל לא דרך האימיול ודומיו, אלא דרך האתר, איך עושים את זה ?
אז התחלתי לחקור את העניין, והתברר לי שאמנם זה לא הכי מסובך, אבל גם לא הכי פשוט.
אז הנה כל התהליך, להנאתכם:
שלב ראשון עליכם להתקין
Fiddler, או כל תוכנה אחרת שיודעת להסתכל על מה שבאמת עובר ברשת, כי אנחנו צריכים לתפוס את ה URL של הוידיאו של ההישרדות, והוא לא מופיע ב ViewSource,
שלב שני אחרי שהעליתם את הדף של ההשרדות, לפני שאתם מפעילים את הסרט (שימו לב שיש דפים שהסרט מתחיל לעבוד אוטומטית, וזה לא טוב, כי אפשר לאבד את ה URL)
הפעילו את ה Fidller, ואז לחצו על הפעלת הוידיאו,.
ה Fiddler יתחיל להראות את המידע שעובר מהרשת אל המחשב שלכם.
בשלב כלשהו (ממש לפני ירידת הסרט עצמו) יופיע החלון הבא:
זה אומר שה URL לסרט הופיע, אבל ה Fiddler לא ממש יודע מה לעשות עם זה.
אשרו את ההודעה, והמשיכו לחכות, אחרי חצי דקה בערך תופיע שוב הודעה (הראשון היה של סרט הפרסומת),
אשרו את ההודע השנייה, והעתיקו את ה URL של השורה האחרונה ב Fidller (אפשר עם Crtl+U)
זהו ה URL לסרט עצמו.
נראה משהו כזה: (פרק 15)
עכשיו אפשר לפתוח אותו בכל נגן מדייה, ולצפות בלי פרסומות.
הבעייה היא שאי אפשר לשמור אותו, ואם אנחנו רוצים לשמור אותו, אנחנו צריכים נגן שיודע לשמור את ה Stream שמגיע.
אחד חינמי כזה הוא VLC media player,
יש לו כבר גרסאות חדשות, אבל התמונות הבאות הם מגירסת 0.8.6e (אפשר למצוא את זה ברשת)
הנה התהליך עבור שמירת המידע:
שלב ראשון, הפעילו את התוכנה:
שלב שני, תחת תפריט File, לחצו על Open Network Sream.
ותקבלו את החלון הבא:
בחרו ב Http/Https/FTP/MMS
העתיקו את ה URL לתוך תיבת הטקסט.
סמנו את Stream/Save.
לחיצה על Settings תפתח את החלון הבא:
סמנו את
Play locally
File,
לחצו על Browse, כדי לבחור שם של קובץ.
סמנו את ASF, ולחצו על OK.
תוכלו לצפות בהישרדות בזמן שזה מוריד ושומר את הסרט, אבל אי אפשר לעצור באמצע.
תהנו.
קבלתי במייל ממישהו בשם פיני את השאלה הבאה: (שיניתי את זה קצת כדי שזה יהיה יותר כללי)
נניח שיש לנו את המבנה הבא:
טבלת מרצים:
טבלת קורסים:
וטבלת מופעים של מרצים בקורסים, כשעמודת type קובעת האם המרצה מעביר את הקורס או רק משתתף בה.
בהינתן השאילתא הבאה:
SELECT
Lecturers.Name, Type, COUNT(*) [count] FROM LecturersINNER JOIN LecCourse ON (LecCourse.LecturerId = Lecturers.Id)
INNER
JOIN Courses ON (LecCourse.CourseId = Courses.Id)
GROUP
BY Lecturers.Name, Type
נקבל את התוצאה הבאה: (בטבלה בדו"ח)
| Name |
Type |
Count |
| tomer |
false |
2 |
| noam |
true |
2 |
| shlomo |
true |
2 |
מה שזה אומר, שנועם ושלמה (שני מרצים) הירצו בשני הרצאות, ותומר השתתף בשניהם.
עכשיו השאלה היא כזאת,
איך אפשר להוסיף תיבת טקסט שתראה את סכום ההרצאות (זאת אומרת לסכום רק את אלה שה type == true).
ברור שמקוד או ב sql זה מאוד פשוט, השאלה היא, האם זה אפשרי גם ב Reporting Services.
והתשובה, שכן.
צריך להוסיף תיבת טקסט, וב value לכתוב את ה Expression הבא:
=Sum(iif(Fields!type.Value =
"True", Fields!count.Value,0), "dbTest")
שזה אומר:
=Sum: בתיבת הטקסט יוצג מה שחוזר מהפונקצייה הזאת.
iif: נכניס משפט בדיקה, לאחריו ביצוע פקודה במידה והבדיקה החזירה true, ולאחריו ביצוע פקודה במידה והבדיקה החזירה false.
הבדיקה בודקת האם בעמודה type יש את המחרוזת True (ה iif רץ עבור כל שורה בטבלה).
במידה וכן נחזיר את הערך שיש בעמודת count.
במידה ולא נחזיר את הערך 0.
בסופו של תהליך בתיבת הטקסט יהיה כתוב 4.
כשראיתי את הקוד הזה:
1 class MyClass
2 {
3 private static int i;
4 static private int j;
5 }
הייתי בטוח ששורה 4 לא תתקמפל,
אבל מסתבר שזה מתקמפל והכל עובד כמו שצריך,
ניסיתי לחפש הבדלים, חקרתי קצת את קוד ה IL שנוצר,
אבל בסופו של דבר תמיד נוצר קוד IL רגיל. (וזהה)
זה נראה לי, שאחד המפתחים של ה Visual Studio, החליט להוסיף את הפיצ'ר הלא ברור הזה,
אם מישהו יודע איזה סוג של הבדל בין השורות,. אשמח לשמוע על זה.
בהרבה מקרים כשאני משתמש עם ה Login Control,
אני רוצה את האפשרות שבזמן פיתוח, יהיה לי לחצן שכשאני לוחץ עליו, הוא יעשה Login עם שם משתמש וסיסמא קבועים מראש,
(אני לא רוצה להשתמש ב Remember Me)
הבעייה, שה Control הנהדר, נותן לי את האפשרות רק לשנות את ה UserName מקוד,
אבל הוא לא נותן את האפשרות לשנות את הסיסמא מקוד, וגם לא נותן את האפשרות לעשות Login מקוד, ולכן.
מה שאני עושה זה לכתוב את הקוד הבא, ב PageLoad:
1 #if DEBUG
2 Button btn = new Button();
3 btn.Text = "Login With 1 and 1";
4 form1.Controls.Add(btn);
5 btn.Click += (sender1, e1) =>
6 {
7 // get the password field
8 Type loginType = Login1.GetType();
9 FieldInfo password = loginType.GetField("_password",
10 BindingFlags.NonPublic | BindingFlags.Instance);
11
12 // set the name and the password
13 Login1.UserName = "1";
14 password.SetValue(Login1, "1");
15
16 // invoke te login methde
17 MethodInfo attemptLogin = loginType.GetMethod("AttemptLogin",
18 BindingFlags.NonPublic | BindingFlags.Instance);
19 attemptLogin.Invoke(Login1, null);
20 };
21 #endif
אני מייצר לחצן בזמן ריצה.
מוסיף אותו ל Collection של ה Controls של ה form.
ובזמן לחיצה עליו, אני מקבל את השדה שמייצג את הסיסמא,
עושה השמה לשם ולסיסמא.
מקבל את המתודה שעושה Login
ומפעיל אותה.
קל ופשוט.
באופן אישי אני מתעב את Ofiice2007,
לא מבין למה מייקרוסופט החליטו למרר לנו את החיים עם הריבון, לא מסתדר איתו, ואני לא רוצה להסתדר איתו, לפחות היו נותנים לנו את האפשרות לבחור.
בכל מקרה עשיתי מצגת מסוימת ב PowerPoint, ועשיתי הנפשה מסוג תנועה.
ואני מגלה שזה זז יותר מידי מהר לטעמי,
אני אני מסתכל על ה Combo ומשנה את זה לאיטי מאוד:
ואז מתברר לי שזה עדיין מהר מ א ו ד.
טוב מה לעשות, אלו האפשרויות שמייקרוסופט החליטו לתת.
אחרי נסיונות נואשים גיליתי שאם לוחצים קליק ימני על האפקט, ובחורים בתיזמון, מקבלים את החלון הבא:
בחלון הזה, ה combo ניתן לעריכה, ואפשר לכתוב כמה שניות רוצים שהאפקט יקח.
אז כמו שהבטחתי, אני רוצה להדגים איך אפשר להוסיף מספרי שורות למטריצה שמוצגת ב ReportingServices,
את הפרויקט תוכלו להוריד
מכאן:
אז לפני שנתחיל עם איך עושים את זה, נציץ לתוצאה הסופית:
הדוגמא עובדת על northwind, כשאנחנו שולפים את כל המוצרים והשנה שבהם הם נקנו, על ידי מי הם נקנו, ומי טיפל בקנייה.
כמו שאפשר לראות, יש לנו עמודה עבור כל עובד
יש שורה עבור כל לקוח
וה data הוא, המוצר שנמכר.
אבל כמו שאפשר לראות, השורות ממוספרות, ומעבר לכך, גם השורות הפנימיות (השנים שבהם הלקוח קנה) ממוספרות במיספור נפרד.
אז איך עושים את זה ?
שלב ראשון יש לנו שאילתא, היא פשוטה ושולפת את המידע,
SELECT LastName + ' ' + FirstName AS name,
ContactName,
ProductName,
YEAR(OrderDate) AS YEAR
FROM dbo.Employees
INNER JOIN dbo.Orders ON Orders.EmployeeID = Employees.EmployeeID
INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID
INNER JOIN [Order Details] ON [Order Details].OrderID = Orders.OrderId
INNER JOIN Products ON [Order Details].ProductId = Products.ProductId
GROUP BY LastName + ' ' + FirstName, ContactName, ProductName, YEAR(OrderDate)
שלב שני נגרור מטריצה לתוך ה body של ה report
נוסיף לשורות את עמודת ה name
נוסיף לעמודות עת עמודת ה ContactName
ול data נוסיף את עמודת ProductName.
עד כאן יצרנו מטריצה רגילה.
עכשיו נתחיל לעבוד על מספרי השורות.
קיימת פונקציה שנקראת RowNumber, שלעיתים אפשר לעבוד איתה, אני לא אדגים אותה כרגע, אפילו שלפעמים אפשר להשתמש בה, במקרה שלנו אי אפשר.
אז כדי שיהיו לנו מספרי שורות, אנחנו צריכים לייצר פונקצייה משלנו, שבכל פעם שנקרא לה, היא תדע להחזיר מספר יותר גדול באחד מהפעם הקודמת.
תחת תפריט Report, נבחר ב ReportProperties, נבחר בטאב של Code ונקבל את החלון הבא:
נוסיף לשם את הקוד הבא:
1 Public Shared MyNum As Integer = 0
2 Public Shared MyNum2 As Integer = 0
3
4 Public Shared Function GetNum() As String
5 MyNum = MyNum + 1
6 Return MyNum.ToString
7 End Function
8
9
10 Public Shared Function GetNum2() As String
11 MyNum2 = MyNum2 + 1
12 Return MyNum2.ToString
13 End Function
14
15 Public Shared Function ResetNum() As String
16 MyNum2 = 0
17 End Function
נעבור על הקוד:
מוגדרים שני משתנים גלובליים (עבור המיספור)
פונקצייה ראשונה שמקדמת את המספר ומחזירה אותו.
פונקצייה שנייה שמקדמת את המספר השני ומחזירה אותו.
פונקצייה שלישית שעושה reset למשתנה השני. (כי המיספור השני, הוא עבור המיספור הפנימי של השנים, ועבור כל לקוח המיספור צריך להתחיל באחד)
כעת נבחר את המטריצה שלנו, נלחץ קליק ימין עם העכבר, ונבחר ב AddRowGroup, נקבל את החלון הבא:
ב expression נוסיף את ה ContactName, (מאחר ואנחנו לא באמת רוצים להוסיף group חדש, אלא רק להוסיף עוד עמודה לכל שורה, ואנחנו רוצים שה groupt יהיה כמו העמודה שמציגה את ה ContactName)
נאשר את הוספת ה group החדש.
כעת נבחר בתיבת הטקסט החדשה שנוצרה, (שכרגע כמובן זהה לזה שמציג את ה ContactName).
ונשנה את המאפיין Value ל:
=Code.GetNum() + Code.ResetNum()
המשמעות של זה:
בכל פעם שצריכים להציג ערך עבור העמודה הזאת,
נפעיל את הפונקצייה GetNum, כדי לקבל את המספר הבא,
וכמובן נפעיל את הפונקצייה ResetNum כדי לאפס את המיספור של השנים, שזה מיספור פנימי עבור כל אחד מה Contacts.
נחזור על התהליך ונוסיף עוד RowGroup כשב expression נוסיף לפי ה year
ונחזור שוב על התהליך ונוסיף עוד RowGroup כשב expression נוסיף לפי ה year וב value נכתוב:
=Code.GetNum2()
כדי לסדר את המיקום של ה RowsGroup, ליצה ימנית על המטריצה, בחירה ב Properties, נקבל את החלון הבא,
שבו אפשר למיין את סדר העמודות:
לפני כמה ימים רציתי להציג ב gridview את התמונות מטבלת categories מתוך northwind,
לכאורה הקוד הוא פשוט ביותר, והוא נראה כך:
הנה קוד ה html:
1 <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False">
2 <Columns>
3 <asp:BoundField DataField="CategoryName" HeaderText="Name" />
4 <asp:BoundField DataField="Description" HeaderText="Description" />
5 <asp:ImageField DataImageUrlField="CategoryId"
6 DataImageUrlFormatString="Handler1.ashx?id={0}"
7 HeaderText="Picture">
8 </asp:ImageField>
9 </Columns>
10 </asp:GridView>
אז הגדרנו grid, שיש לו שלושה עמודות,
השניים הראשונים פשוטים, הם מקושרים לעמודת ה name ולעמודת ה description,
בשורה 5 הגדרנו עמודה מסוג Image, שה url שלו מפנה ל handler (שעוד מעט נראה אותו), כשה id מתקבל מתוך ה categoryId,
כעת נראה את הקוד:
11 protected void Page_Load(object sender, EventArgs e)
12 {
13 if (!IsPostBack)
14 {
15 string connectionString = @"server=localhost;
16 initial catalog=northwind;
17 integrated security=true";
18
19 SqlConnection cnn = new SqlConnection(connectionString);
20 SqlCommand cmd = new SqlCommand("SELECT * FROM Categories", cnn);
21 cnn.Open();
22
23 SqlDataReader dr = cmd.ExecuteReader();
24 DataTable dt = new DataTable();
25 dt.Load(dr);
26 cnn.Close();
27
28 Application["data"] = dt;
29
30 GridView1.DataSource = dt;
31 GridView1.DataBind();
32 }
33 }
מתחברים ל database שולפים את טבלת categories,
יוצרים את הטבלה מתוך ה reader, ומחברים את ה gridview, לטבלה.
כעת נראה את ה handler:
34 [WebService(Namespace = "http://tempuri.org/")]
35 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
36 public class Handler1 : IHttpHandler
37 {
38
39 public void ProcessRequest(HttpContext context)
40 {
41 DataTable dt = (DataTable)context.Application["data"];
42
43 string expression = string.Format("CategoryId = {0}",
44 context.Request["id"]);
45 DataRow row = dt.Select(expression)[0];
46 byte[] array = (byte[])row.ItemArray[3];
47
48
49 context.Response.ContentType = "image/bmp";
50 context.Response.OutputStream.Write(array, 0, array.Length);
51 }
52
53 public bool IsReusable
54 {
55 get { return false; }
56 }
57 }
58 }
המתודה החשובה היא ProcessRequest,
שולפים מתוך ה Application את ה dataTable,
שולפים מתוך הטבלה את הרשומה לפי ה CategoryId
לוקחים את המערך הבינארי מהעמודה השלישית.
משנים את ה ContextType
וכותבים את המערך ל Response.
עד כאן הכל רגיל, אבל..
הנה התוצאה:
מה קרה, לכאורה הקוד שכתבנו, צריך לעבוד ?
התשובה טמונה, בזה שה database שלי היה מ 2000 שה northwind שלו, נלקח מאקסס, ולא שוכתב מחדש כמו ב 2005,
ובאקסס כל מידע בינארי נשמר כ OLE שזה מוסיף לתחילת המערך 78 בתים שהם ה header של OLE,
ולכן הקוד שלנו צריך להיראות כך:
1 public void ProcessRequest(HttpContext context)
2 {
3 DataTable dt = (DataTable)context.Application["data"];
4
5 string expression = string.Format("CategoryId = {0}",
6 context.Request["id"]);
7 DataRow row = dt.Select(expression)[0];
8 byte[] array = (byte[])row.ItemArray[3];
9
10
11 context.Response.ContentType = "image/bmp";
12 context.Response.OutputStream.Write(array, 78, array.Length - 78);
13 }
שימו לב לשורה 12: (אנחנו מתעלמים מ 78 הבתים הראשונים)
וכשנריץ נקבל את התוצאה המבוקשת: