טפסי WebForms רבים המכילים שדות להזנת נתונים ע"י המשתמש, מכילים גם פקדי וולידציה שתפקידם לוודא את תקינות המידע שהוזן בשדות ע"י ה client.
וולידציה יעילה מבצעת בדיקה בצד לקוח לפני שליחת הטופס לשרת ע"מ לחסוך round-trips מיותרים, של טפסים שלא מולאו בצורה תקינה.
לפני שמתבצע Post-Back של ה form, נבדקים שדות הקלט ובמידה והכל תקין, יישלח הטופס לבדיקה נוספת בשרת ולאחמ"כ לביצוע הפעולה הרצוייה.
במידה ויש לנו צורך לבצע בדיקות נוספות מלבד הבדיקות הרגילות, נוכל לקשור client-side event handler נוסף לפקד שאמור לבצע את ה Post-Back.
נניח שהכפתור שמבצע את ה Submit לשרת נקרא btnAddRecord, נוכל להוסיף לו את ה JS function שהוא צריך להפעיל לפני ה submit, ורק במידה שהכל תקין, הוא רשאי להמשיך.
protected void btnAddRecord_Init(object sender, EventArgs e)
{
string attribute = string.Format(
"return validateweigth('{0}')",
this.ControlToValidate.ClientID,);
(sender as Button).Attributes.Add("onclick", attribute);
}
באירוע Init של הכפתור, הוספנו לו שם של פונקציית JS שמקבלת clientID של פקד צד-שרת שעליו היא תבצע בדיקות ולידציה, מחזירה ערך בוליאני לפי תוצאות הבדיקה.
לפני שהכפתור ימשיך לביצוע Post Back הוא יחכה ל return value מהפונקצייה.
הבעיה:
נניח שיש לנו עוד כמה שדות קלט שאמורים לעבור וולידציה ע"י פקדי ASP.NET Validators כגון RequiredField Validators, Regex Validators וכו', ויחד יש לנו גם את ה js function הנ"ל על הכפתור שאמור לשמור את הנתונים לשרת, ייתכנו שני מצבים: או שנקבל תשובה שלילית במידה והבדיקה נכשלה, או שוולידציה תסתיים בהצלחה ונקבל תשובה חיובית.
אם קיבלנו תשובה שלילית, כמובן שלא ייבצע Post Back והכל טוב ויפה. אך מה יקרה אם נקבל תשובה חיובית? או אפילו לאו דווקא תשובה חיובית בפועל, אלא מספיק שלא נקבל חזרה false, מבחינת הכפתור הכל בסדר והוא יריץ אותנו לשרת.
זו הבעייה, שכל שאר הוולידציה ב Client-Side לא תתבצע וישר נגיע לשרת, ובעצם בגלל שלא החזרנו לכפתור false, הפסדנו את שאר ה client-side validation של פקדי הוולידציה כיוון שהכפתור חושב שהכל בסדר ואפשר להמשיך הלאה.
איך בכל זאת דואגים לוולידציה בצד ה client?
שני אפשרויות:
א. שימוש ב CustomValidator, ואז לא צריך לקשור את הפונקצייה לכפתור לפני ביצוע post back
ב. לעיתים, יותר נוח להגדיר את הבדיקה על הכפתור ולוותר על השימוש ב CustomValidator, בכדי לפתור את הבעייה, עושים דבר פשוט: בסוף הפונקצייה, בודקים אם הכל עבר בשלום. במידה וכן, לפני שאנחנו מחזירים לכפתור true והוא יברח לנו לשרת ללא בדיקה ב client-side, ישנה מתודת JS מוכנה שתפקידה לבצע את ה client-side validation.
ניתן לקרוא למתודה שתבצע את הוולידציה, בצורה כזו:
Page_ClientValidate();
ואפשר גם להשתמש ב overload שמקבל את ה validation group הרצוי כפרמטר. לדוגמא:
Page_ClientValidate('MyValidationGroup');
אחרי שהמתודה נקראה, גם אם לא נחזיר false, הכפתור לא ימשיך לשרת לפני שביצוע וולידציה הושלם, גם אצל ה client.
בעייה: כשעובדים עם הפקדים של MS AJAX Toolkit לעיתים קורה מצב שבעליית הטופס
פקדים מסויימים, שאמורים לעלות במצב "collapsed" עולים "expanded" לחלקיק השניה
ומיד נסגרים.
בד"כ זה לא קורה והפקדים עובדים בסדר, אך הפעם היה לי צורך לשתול באופן דינמי alert
של javascript שאומר שהנתונים עודכנו בהצלחה לאחר ביצוע פעולת שמירה.
מכיוון שה alert עולה ב load של הטופס, חל עיכוב בטעינת ה form ובעצם מה שקרה זה
שה CollapsiblePanel שלי, "נתקע" במצב "expanded" במקום "collapsed" עד לאחר
ה "ok" של ה alert.
מעיק.
הפתרון היה לשחק עם ה style של ה panel ולשנות את הגדרת ה display שלו לפני ואחרי
הצגת ה alert.
הקוד שמבצע את שתילת ה alert:
public void ShowJSAlert(string message)
{
StringBuilder script = new StringBuilder();
script
.Append("<script type=\"text/javascript\">")
.Append(Environment.NewLine)
// Get the panel control
.Append("var panel = $get('")
// Get the collapsible panel clientID
.Append(this.panelContent.ClientID)
.Append("');")
.Append(Environment.NewLine)
// Set its display to 'none' to avoid blink
.Append("panel.style.display= 'none';")
.Append(Environment.NewLine)
.Append("alert('")
.Append(message)
.Append("');")
.Append(Environment.NewLine)
// Display it back for repeated use
.Append("panel.style.display= 'block';")
.Append(Environment.NewLine)
.Append("</script>");
ScriptManager.RegisterClientScriptBlock(
this, this.GetType(), "alert",
script.ToString(), false);
}
בקוד הזה, יוצרים את ה script שישתל ואת ההודעה שתוצג ב alert.
בהתחלה מאתרים את ה panel הבעייתי, ומגדירים את ה style.display שלו כ 'none'.
עכשיו כשנציג את ה alert הוא לא ייפתח.
כמובן שמיד אח"כ חשוב להחזיר אותו למצב הרגיל (בעזרת 'style.display = 'block) וזאת על מנת
שיהיה ניתן להמשיך להציג אותו בעזרת ה CollapsiblePanelExtender.
עכשיו אין blink מרגיז של ה panel בהצגת אישור על ביצוע שמירה.
לפני כמה ימים הייתה לי בעייה:
ביצעתי פעולת עדכון נתוני משתמש ל Data Source, ולאחר בדיקה שהעדכון אכן בוצע, שתלתי js alert שהעדכון אכן בוצע בהצלחה.
הבעיה הייתה שהסקריפט לא הופיע בדף אחרי העדכון, ובדיקה ב view source הראתה שהוא בכלל לא נשתל.
מכיוון שהמערכת שלי מציגה נתוני משתמש לפי ID מה QueryString, לאחר יצירת משתמש חדש הייתי מבצע Response.Redirect ל URL + ה User ID החדש.
מה שהיה קורה, זה שה alert היה נשתל בדף הישן, ואני הייתי נשלח לדף החדש.
אז חשבתי, איך אני יכול לשתול JS alert בדף שאני רוצה לעבור אליו.
פתרתי את הבעייה בעזרת שני פונקציות פשוטות:
הראשונה: יצרתי פונקצייה שמקבלת string message ומשרשרת אותו עם שאר הסינטקס ליצירת ה alert ואחרי שהסקריפט היה מושלם, הכנסתי אותו למשתנה session.
private void CreateSessionBasedJSAlert(string message)
{
StringBuilder script = new StringBuilder();
script
.Append("<script type=\"text/javascript\">")
.Append("alert('")
.Append(message)
.Append("')</script>");
Session.Add("JSMessage", script.ToString());
}
לפונקצייה השניה קראתי מידי Page_Load, ופשוט בדקתי אם יש message כלשהוא להצגה:
private void ShowJSAlert()
{
// Check for messages in the session variable
string message = (Session["JSMessage"] != null)
?Session["JSMessage"].ToString()
: string.Empty;
// If there's any message
if (message.Length > 0)
{
// Get the current executing page
Page page = HttpContext.Current.CurrentHandler as Page;
// Check if the handler is a page and the script
// isn't already on the page
if (page != null &&
!page.ClientScript.IsClientScriptBlockRegistered("alert"))
{
// plant the script on the page
page.ClientScript.RegisterClientScriptBlock(
this.GetType(), "alert", message);
}
// Remove the used script (To avoid duplication of the
// message every postback)
Session.Remove("JSMessage");
}
}
לא מתוחכם מידי, אבל מאוד עזר לי :-)
היי כולם.
בפוסט הזה נסביר איך ליצור דפי web בארכיטקטורת ASP.NET MVC (גירסת ה CTP 3).
הקדמה ל ASP.NET MVC :
ארכיטקטורת ה ASP.NET MVC מבצעת חלוקה באפליקציות ווביות בין ה UI לבין ה Data
שאמור להיות מוצג בדפי אינטרנט, ע"י מידול ה Web Application שלנו לשלוש שכבות:
Model, View And Controller.
ה View אמור להציג את הנתונים בלבד. View לא מכיר שום מקור נתונים ולא מבצע שום
מניפולציה על Data. תצוגה נטו. View אמור להיות "טיפש" ככל שניתן.
ה Model מנהל את ה business logic של האפליקצייה ואת הגישה למקור הנתונים.
ה Controller מגשר בין ה Model וה View ודואג לבצע את השליפות בעזרת ה Model
ואת שתילת הנתונים שהתקבלו לאחר ביצוע הפעולות הדרושות, אל תוך ה View.
דבר ראשון, ניצור אפליקציית ASP.NET MVC חדשה:
[ניתן לוותר על יצירת Test Project בדוגמא זו]
בברירת המחדל, קיבלנו template המכיל כבר Controller ו View מוכנים.
יצירת ה Controller:
בספריית ה controllers נוסיף item חדש מסוג "MVC Controller Class" , ונקרא לו בשם DataController.
[חובה לציין את הסיומת "controller" בסוף השם].
ב controller החדש שיצרנו נקבל Action שנקרא "Index", שמחזיר ActionResult.
ע"מ שלא נחטוף Exception כשנכנס ל action הזה, נממש אותו בצורה הבאה:
public ActionResult Index()
{
ViewData["Title"] = "Data Controller Page";
ViewData["Message"] = "Welcome to ASP.NET MVC!";
return View();
}
בנוסף, יש להעתיק את הקובץ Index.aspx ואת קובץ ה code-behind שלו מהספרייה "Home" אל הספרייה "Data" ע"מ שיהיה View שה action יוכל להתרנדר אליו.
בגלל ההבדלים במיקום הקובץ יש לשנות ב Page Directive את ה Inherits ב Markup, ואת הגדרת
ה namespace ב code-behind במקום MyMvcWebSite.Views.Home לערך החדש MyMvcWebSite.Views.Data
עכשיו ניצור שני actions חדשים משלנו, ונקרא להם ProductInfo, ו CategoryInfo:
public ActionResult ProductInfo(int? id)
{
if (id.HasValue)
{
// Get the Product object from the Model layer
NorthwindDataService service = new NorthwindDataService();
Product product = service.GetProduct(id.Value);
ViewData["ProductName"] = product.ProductName;
ViewData["categoryID"] = product.CategoryID;
}
return View();
}
public ActionResult CategoryInfo(int? id)
{
if (id.HasValue)
{
// Get the Category object from the Model layer
NorthwindDataService service = new NorthwindDataService();
Category category = service.GetCategory(id.Value);
ViewData["categoryName"] = category.CategoryName;
ViewData["categoryDesc"] = category.Description;
}
return View();
}
בכדי לשלוף את הנתונים אנו עושים שימוש במחלקה NorthwindDataService שנמצאת ב Model Layer.
ספריית ה Models לא ניגשת בשום צורה ל Views או מטפלת בקריאות מתוך ה Views באופן ישיר.
ברמת העיקרון, ספריית ה Models מטפלת ב business logic של האפליקצייה שלנו, מכילה את הגדרת הישויות שיצרנו ומנהלת את התקשרות בין האפליקצייה למקור הנתונים.
לכן ב action methods אנו ניגשים למחלקה NorthwindDataService והיא זו שניגשת ל DB ושולפת עבורנו את ה Data הרצוי.
הסבר על פעולות ה action methods:
כשאנו עובדים ב ASP.NET MVC וגולשים ל URL מסויים, מנוע ה Routing (עליו נרחיב בהמשך) הופך את הכתובת שנמצאת ב address bar להפנייה ל Controller/action/parameter לפי מה שמוגדר by default. מנוע ה routing יודע להפנות אותנו ל controller המבוקש, ובתוכו ל action method שאמורה לטפל ב request הנוכחי. במידה ויש פרמטר הוא מתקבל ב parameter list של ה method.
בכדי שלא נקבל שגיאה במידה ולא התקבל פרמטר משורת הכתובת, אז הפרמטר צריך להיות reference type או nullable value type. אחרת, במידה והעברת הפרמטר כשלה, תתקבל שגיאה במע'.
אחרי שליפת הנתונים וקבלת ה entity המתאים, אנו מאתחלים dictionary מסוג ViewDataDictionary שמכיל את האובייקטים שיועברו ל UI בהמשך ומאוסנים לפי אינדקס כרגיל ב Dictionary.
בקריאה ל ()return View ה ViewData עובר ל View המתאים.
יצירת ה View:
אחרי שיצרנו את ה action של ה controller ניצור את ה View שיציג את ה rendered html שה controller מייצר לנו ע"י ה action בו השתמשנו.
נתחיל בהוספת תקייה חדשה לתקיית ה Views.
שם התקייה חייב להיות זהה לשם ה controller אותו אנו מעוניינים להציג, אך ללא הסיומת "controller". לכן, אם ה controller שלנו היה "DataController", שם התת-הספרייה יהיה "Data".
לתקייה נוסיף שני items חדשים מסוג MVC View Content Page (ע"מ שיירש את ה MasterPage שנמצא בתקייה Views=>Shared) ונקרא להם בשמות התואמים ל action methods שלהם.
לכל action method יש את ה view משלו.
[ניתן להשתמש ב view יחיד לכמה actions אך לצורך הדוגמא ניצור לכל action method את ה view שלו]
ה items שנוספו (MVC View Content Pages) יורשים מהמחלקה ViewPage שיורשת מ Page ומממשת את IViewDataContainer.
ירושה מ IViewDataContainer חושף לנו את ה ViewData property שמנגיש לנו את הערכים שהזנו ב controller.
עכשיו מבנה ה Solution שלנו נראה כך:
בשונה מ WebForms אין צורך להגדיר דבר ב code-behind של דפי ה aspx. הדבר היחיד שחשוב לנו שם הוא ההגדרה שהמחלקה שלנו יורשת מ ViewPage.
public partial class ProductInfo : ViewPage
{
}
ה Page Directive של CategoryInfo / ProductInfo יראה כך:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="ProductInfo.aspx.cs" Inherits="MyMvcWebSite.Views.Data.ProductInfo" %>
מבנה הגדרת ה Inherit ב Page Directive בנוי כ Application.Views.Controller.View
ולכן הערך שלו יהיה MyMvcWebSite.Views.Data.ProductInfo.
ה Markup שלנו עושה שימוש ב Helpers ע"מ לייצר את ה HTML.
בכדי לשלוף את המידע ששתלנו ב Controller ולהפוך אותו ל HTML נבצע את הפעולות הבאות:
[ProductInfo.aspx Markup]
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>Product Name:
<%= Html.Encode(ViewData["ProductName"]) %></h2>
<%= Html.ActionLink("Link to category",
"CategoryInfo",
new RouteValueDictionary{
{"id", ViewData["categoryID"]}}
)%>
</asp:Content>
[CategoryInfo.aspx Markup]
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>Category Information</h2>
<p><b>Category Name:</b>
<%= Html.Encode(ViewData["categoryName"])%></p>
<p><b>Category Description:</b>
<%= Html.Encode(ViewData["categoryDesc"])%></p>
</asp:Content>
About Helpers:
ישנם כמה "Helpers" שעוזרים לנו לייצר את ה UI ולגשת לערכים הקיימים ב ViewData (אותם
שתלנו ב controller).
יש את המתודה (Html.Encode(object value שמייצרת לנו את ה HTML מתוך האובייקט
אותו אנו מספקים כפרמטר.
לדוגמא, בעמוד ProductInfo אנו ניגשים לערך שנמצא ב ViewData Dictionary
באינדקס "ProductName". הערך שנקבל יהיה string שאותחל ב Controller. אותו נהפוך ל HTML
ע"י שימוש ב ()Html.Encode וע"י כך אנו יוצרים את התוכן של תגית <h2>.
בהמשך אנו יוצרים ActionLink, שוב ע"י ה Html Helper. אנו מספקים את ה like text כפרמטר ראשון,
את ה "action" אליו אנו פונים, ולאחמ"כ את ה values אותם אנו רוצים להעביר ל action method אליו
אנו פונים ע"י ה link. ה Target URL מיוצר אוטומטית ע"י מנגנון ה routing של ASP.NET MVC
לפי הכללים שהגדרנו בהרשמת ה Routes ל route collection של האפליקצייה שלנו.
CategoryInfo.aspx עושה שימוש דומה למדי ב Html Helpers כמו ProductInfo.aspx.
זהו.
יש לנו עכשיו אפליקציית MVC מוכנה.
נשנה את ה URL ל 1/http://server/Data/ProductInfo
מה שיקרה עכשיו, זה שה Data Controller ייקרא לפעולה, יבצע Invoke ל action method
המתאים => ProductInfo ויעביר את הפרמטר "1".
נקבל את העמוד שמכריז על שם המוצר, ואת הלינק שמוביל לקטגוריה אליה הוא שייך.
לחיצה על הלינק פותחת את העמוד Category Information עם שם הקטגורייה ותיאורה.
בהצלחה,
תמיר.