Custom web controls
יצא לי בזמן האחרון לבנות כמה קונטרולים נחמדים. אני נוהג לעיתים תכופות להשתמש בקונטרולים שאני בונה,
אך לאחרונים הייתי צריך כמה תכונות נוספות, שלא כל כך ברור לכולם כיצד ליצור אותם, אז חשבתי להראות קצת את המהלכים הנדרשים.
נתחיל הפעם ביצירת הקונטרול בעזרת השם אני אמשיך אחר כך עם הדברים שרציתי להראות.
ישנם 2 סוגים של ווב קונטרולים שאנו יכולים ליצור. ניתן ליצור את הקונטרול באתר שלנו כמו כל דף אחר, על ידי
הוספת פריט חדש
או שניתן ליצור פרוייקט חדש, והתוצאה שלו תהיה dll שניתן להוסיף לאתר שלנו
יש הבדלים בין השיטות. בדרך הראשונה, הקונטרול יהיה זמין רק לאתר שבו יצרתי אותו, אני לא אוכל להשתמש בו באתרים אחרים. מצד שני, כל תהליך הבניה הוא קל יותר כי לא צריך לקמפל מחדש כל פעם , הוא מתקפל אוטומטית כמו כל שאר הדפים באתר. הקונטרול בצורה הזאת יורש מהמחלקה
System.Web.UI.UserControl
בשיטה השניה זה הפוך כמובן... אני יכול להוסיף הפניה ל dll שנוצר מכל פרוייקט ווב שארצה, אך מצד שני פחות נוח בתהליך הפיתוח לקמפל מחדש כל פעם את הפרוייקט. ישנם עוד הבדלים קטנים בצורת כתיבת הקוד ובקריאת לאירועים של הקונטרול .
הקונטרול בצורה הזאת יורש מ
WebControl
אנו נבחר בשיטה הראשונה... ניצור פרוייקט מסוג WebCustomControl .
כעת נשנה את השם של המחלקה שנוצרה באופן אוטומטי כי השם הזה הוא חלק ממה שהמשתמש יראה כשהוא ירצה לזרוק
את הקונטרול על דף הווב שלו, וניבחר משהו כמו LightCaptchaValidator
קיבלנו מחלקה שנראית כך:
[ToolboxData("<{0}:LightCaptchaControl runat=server></{0}:LightCaptchaControl>")]
public class LightCaptchaControl : WebControl
רק לפני שנמשיך צריכים לעשות איזו שינוי קטן, תוסיפו עוד Interface כך שהגדרת המחלקה תיראה כך:
public class LightCaptchaControl : WebControl, INamingContainer
שאינה רלוונטית לקונטרול שלנו. כפי שניתן לנחש, התגית ToolboxData אומרת כיצד יראה הקוד בדף ה aspx לאחר שנזרוק את הקונטרול על הדף, כש {0} זה הערך שניתן בדף ל TagPrefix
עוד יש לנו במחלקה דוגמה לפרופרטי שניתן לערוך ב Form Designer. ה Form Designer בעצם מציג את כל הפרופרטיס שיש לנו במחלקה ולכן אם אנו רוצים "להעלים" מהמשתמש משהו, יש להוסיף תגית
מעל פרופרטי שאמור להיות מוחבא. התגית:
אומרת פשוט באיזה קטגוריה יוצג הפרופרטי הזה ו DefaultValue הוא כמובן ערך ברירת המחדל... שימו לב שבפרופרטיס של ווב קונטרול, שלא כמו בפרופרטיס של מחלקה רגילה, אנו שומרים ומושכים את המידע מה ViewState, ולא ממשתנה מחלקה, כי הרי הקונטרול הזה נוצר מחדש בכל פעם שהמשתמש מבקש את הדף או עושה פוסטבאק לשרת, והדרך היחידה לשמור על ערך המשתנים הוא ב ViewState.
המתודה שיש לנו במחלקה:
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
המתודה הזאת נקראת על ידי הדף כשהוא מוכן לשלב את פלט ה HTML של הקונטרול שלנו בתוכו.
היא המתודה שבה אנו כותבים את הפלט הסופי של הקונטרול שלנו וזהו בעצם ה HTML שיוצג על הדף. כפי שניתן לראות המחלקה מקבלת פרמטר מסוג HtmlTextWriter(כתבתי עליו כמה מילים במקום אחר).
אם נשים את הקונטרול עכשיו על דף ונריץ, נקבל בעצם טקסט ריק, כי זה מה שהמתודה הזאת מדפיסה לדף
טוב, נמשיך. מה יעשה הקונטרול שלנו? הוא יהיה כמו captcha זולה... אנו נציג טקסט בוקס, עם שאלה במתמטיקה לידו, והגולש אמור להכניס את התשובה. כשעושים פוסטבאק לשרת הקונטרול שלנו בודק את התשובה ויכול להודיע לדף שאכן היא נכונה ורוב הסיכויים שהגולש איננו סקריפט אוטומטי. כמובן שאי אפשר להציג את השאלה בצורת טקסט פשוט כי אז הסקריפט פשוט ימצא את הפיתרון, אבל זו כבר בעיה אחרת שאולי יהיה לנו זמן גם לפתור...
אז אנחנו צריכים לייבל, שיודיע לגולש שהוא אמור לענות על השאלה שלנו, טקסט בוקס שאליו יכניסו את התשובה, ועוד לייבל שיציג את התרגיל במתמטיקה. כמובן שאפשר להוסיף ולידטור, ואפשר אפילו להוסיף לינק כדי שאפשר יהיה לבחור תרגיל אחר אבל זה לא עכשיו...
כדי להוסיף קונטרולים אחרים לקונטרול שלי , אני צריך להוסיף מתודה אחרת שנקראת על ידי הדף והיא:
protected override void CreateChildControls()
{
}
שימו לב שכאן אנו רק מוסיפים את הקונטרולים, אבל במתודה RenderContents אנו גם נבקש מכל אחד מהם לרנדר את עצמו לתוך ה HTML של כל הקונטרול שלנו...
protected override void CreateChildControls()
{
////the label with the text asking
///the user to answer the question
Label lblText = new Label();
lblText.Text = this.Text;
lblText.ID = "lblText";
this.Controls.Add(lblText);
////the text box with the answer
TextBox txtAnswer = new TextBox();
txtAnswer.ID = "txtAnswer";
this.Controls.Add(txtAnswer);
////The label with the methematics question
Label lblQuestion = new Label();
lblQuestion.ID = "lblQuestion";
this.Controls.Add(lblQuestion);
}
ללייבל lblQuestion עדיין לא שמתי ערך טקסט כי עדיין לא חשבנו איך ניצור את התרגיל המתמטי, אבל נחזור לזה אחר כך.
lblText קיבלה את ערך הטקסט של הפרופרטי Text מהקונטרול שלנו, כך שכשהמשתמש יעצב את הקונטורל וישנה לו את ערך הטקסט, זה מה שיוצג לגולש.
בואו נוסיף אתר לפרוייקט שלנו, כדי שנוכל לראות את הקונטרול שלנו בפעולה(או שאפשר להוסיף הפניה ל dll מאתר קיים כמובן)
נלך לעיצוב הדף, ארגז כלים, לחצן ימני - choose items כדי להוסיף את הקונטרול שלנו לארגז הכלים
בחלון שנפתח נלחץ על Browse ונחפש את תיקיית הפרוייקט שלנו, שם בתוך תיקיית bin\debug נמצא את ה dll שלנו. אם הוא לא שם אז לא עשיתם build לפרוייקט או שתחפשו בספריית release. לחיצה על OK והקונטרול מופיע בארגז הכלים. תמשכו אותו לדף. אם יש איזו בעיה אז אתם תראו, אם לא, אז אנו רואים דף חלק... ואיפה הקונטרולים שלנו? זה שהוספנו אותם זה לא מספיק עדיין, אנו צריכים לרנדר אותם...
אז בחזרה לקונטרול שלנו, כבר יש שם פונקציה מתאימה:
protected override void RenderContents(HtmlTextWriter output)
אפשר לרנדר בריצה בלולאה:
for (int i = 0; i < this.Controls.Count; i++)
{
this.Controls[i].RenderControl(output);
}
אבל אני מעדיף למצוא כל אחד מהקונטרולים ולרנדר אותו:
Label lbl = (Label)this.FindControl("lblText");
lbl.RenderControl(output);
כך תעברו על כל הקונטרולים שלנו.
בצורה הזאת נוכל לשנות מאפיינים בהמשך...
עכשיו אם נחזור לטופס שלנו אנו אמורים לראות טקסט בוקס (את הלייבלים לא נראה כי לא נתנו להם ערך טקסט) אם אתם לא רואים תעתיקו מחדש את ה dll של הקונטרול לתיקית bin של הפרוייקט(
תנו ערך למאפיין טקסט על הטופס שלכם
רפרש קטן על הדף )לחצן ימני( ואתם אמורים גם לראות את הטקסט ליד הטקסט בוקס. זה יהיה גם רעיון טוב לתת ערך ברירת מחדל למאפיין הזה, אז בחזרה לקונטרול, תלכו למאפיין Text, יש שם תגית בשם DefaultValue, תנו לה איזה ערך שתרצו, וגם ב Get של המאפיין תחזירו את אותו טקסט במקרה והוא ריק:
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("Please enter the answer")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? "Please enter the answer" : s);
}
set
{
ViewState["Text"] = value;
}
}
יופי, עכשיו אפשר להתקדם.
אנו רוצים להציג לגולש איזה תרגיל מתמטי כדי לבדוק שאכן זה אדם ולא סקריפט. נבנה מחלקה שתבנה תרגיל מתמטי. רק קודם נבנה איזה מחלקה קטנה שתייצג לנו את התרגיל.
והמחלקה שלנו:
public class Question:ICloneable
{
private double _answer;
public double Answer
{
get { return _answer; }
set { _answer = value; }
}
private List<string> _items;
public List<string> Items
{
get {
if (_items == null)
{
_items = new List<string>();
}
return _items; }
set { _items = value; }
}
#region ICloneable Members
public object Clone()
{
Question q = new Question();
q.Answer = this.Answer;
q.Items = new List<string>();
q.Items.AddRange(this.Items);
return q;
}
#endregion
}
המחלקה מכילה בעצם מערך עם הנתונים שצריך להציג לגולש (מספרים וסימני חשבון) ואת התוצאה הסופית של התרגיל.
עכשיו ניצור לנו מחלקה שתייצר את התרגיל ותמלא אותו בנתונים:
public class QuestionsGenerator
{
private Question _question;
private Question Question
{
get
{
if (_question == null)
{
Generate();
}
return _question;
}
}
private int _itemsCount;
public int ItemsCount
{
get
{
return (int)Math.Max(_itemsCount, 2);
}
set { _itemsCount = value; }
}
private int _maxNumber;
public int MaxNumber
{
get
{
return (int)Math.Max(10, _maxNumber);
}
set { _maxNumber = value; }
}
public QuestionsGenerator()
{
this.MaxNumber = 10;
this.ItemsCount = 4;
}
public double Result
{
get
{
return Question.Answer;
}
}
public Question Source
{
get
{
return (Question)Question.Clone();
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < this.Question.Items.Count; i++)
{
sb.Append(Question.Items[i]);
sb.Append(" ");
}
return sb.ToString();
}
private void Generate()
{
this._question = new Question();
Question.Answer = new Random().Next(0, MaxNumber);
Question.Items.Add(Question.Answer.ToString());
for (int i = 0; i < ItemsCount-1; i++)
{
AddPair();
}
}
private enum Sign
{
Plus,
Minus,
Multiply,
Divide
}
private void AddPair()
{
System.Security.Cryptography.RandomNumberGenerator rng = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] b=new byte[1];
rng.GetBytes(b);
int next = (int)b[0] % MaxNumber;
Sign s = (Sign)Enum.Parse(typeof(Sign), ((int)b[0]%4).ToString());
switch (s)
{
case Sign.Plus:
Question.Answer = Question.Answer + next;
Question.Items.Add("+");
break;
case Sign.Minus:
Question.Answer = Question.Answer - next;
Question.Items.Add("-");
break;
case Sign.Multiply:
Question.Answer = Question.Answer * next;
Question.Items.Add("*");
break;
case Sign.Divide:
bool found = false;
for (int i = 0; i < 5; i++)
{
if (Question.Answer % next != 0)
{
next++;
}
else
{
found = true;
break;
}
}
if (!found)
{
next = 1;//dont' get complicated now...
}
Question.Answer = (int)(Question.Answer / next);
Question.Items.Add("/");
break;
}
Question.Items.Add(next.ToString());
}
}
מחלקה פשוטה שמגרילה כמה מספרים וכמה פעולות מתמטיות ומחשבת את התוצאה.
כעת נחזור לקונטרול שלנו וניתן לליבל שמכילה את התרגיל את הטקסט שלה, בדיוק לפני הרינדור
lbl = (Label)this.FindControl("lblQuestion");
QuestionsGenerator qg = new QuestionsGenerator();
lbl.Text = qg.ToString();
lbl.RenderControl(output);
נמשיך...
אנו צריכים לשמור את התוצאה של התרגיל ב Session של הגולש כדי שנידע אם הוא ענה נכון, אז נוסיף עוד שורה קטנה:
lbl = (Label)this.FindControl("lblQuestion");
QuestionsGenerator qg = new QuestionsGenerator();
HttpContext.Current.Session.Add("LightCaptchaControl_answer", qg.Result);
lbl.Text = qg.ToString();
lbl.RenderControl(output);
עכשיו נוסיף פרופרטי שיאפשר לדף שבתוכו הקונטרול לבדוק אם התוצאה אכן תקינה
[Browsable(false)]
public bool IsValid
{
get
{
double result;
if (double.TryParse(((TextBox)this.FindControl("txtAnswer")).Text, out result))
{
if (HttpContext.Current.Session["LightCaptchaControl_answer"] != null)
{
return result == (double)HttpContext.Current.Session["LightCaptchaControl_answer"];
}
}
return false;
}
}
וזהו... עכשיו מי שמשתמש בקונטרול צריך בסך הכל לבדוק את הפרופרטי הזה:
protected void btnSubmit_Click(object sender, EventArgs e)
{
bool valid = this.LightCaptchaControl1.IsValid;
}
נראה לי שעדיף לנו גם ליצור פונקציה שתערבל קצת את התצוגה של התרגיל, כי הרי מי שבונה סקריפט אוטומטי למילוי טפסים יצטרך בסך הכל להשקיע עוד 5 דקות כדי לקרוא את התרגיל שלנו ולהכניס את התשובה הנכונה, אז הוספתי פונקציה פשוטה, אבל שתדרוש קצת יותר מאמץ מבונה הסקריפטים, אתם יכולים לסבך אותה עוד לפי אותו עיקרון:
public string ToRandomHtml()
{
char[] chars="abcdefghijklmnopqrstuvwxyz1234567890".ToCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < this.Question.Items.Count; i++)
{
System.Security.Cryptography.RandomNumberGenerator rng = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] b = new byte[1];
rng.GetBytes(b);
int ran = (int)b[0] % 5;
switch (ran)
{
case 0:
sb.Append(Question.Items[i]);
sb.Append(new String(' ',new Random().Next(4)));
break;
case 1:
sb.Append(Question.Items[i]);
sb.Append("<span style=\"display:none\">");
sb.Append(Question.Items[i]);
sb.Append("</span>");
break;
case 2:
sb.Append("<span style=\"display:none\">");
sb.Append(Question.Items[i]);
sb.Append("</span>");
sb.Append(Question.Items[i]);
break;
case 3:
sb.Append("<b>");
sb.Append(Question.Items[i]);
sb.Append("</b>");
break;
case 4:
sb.Append("<u>");
sb.Append(Question.Items[i]);
sb.Append("</u>");
break;
}
}
return sb.ToString();
}
אז רק צריך לשנות את הלייבל שמציגה את התרגיל כך שתקבל את הטקסט מהפונקציה הזאת.
כמובן שאפשר להרחיב ולהוסיף שליטה על העיצוב על ידי המשתמש, שליטה על אורך התרגיל להצגה
וכו', בזמנכם החופשי...
להורדת הפרוייקט ודוגמה