במאמר הזה נציע מספר פתרונות מעשיים למקרים בהם צריך להציג שעה מסוימת למשתמשים באזורי זמן שונים.
ניקח לדוגמה מערכת פורום. דוד, מזין הודעה בשעה 13:00 ממחשב בישראל (GMT +2), אלי שגר באנגליה בגריניץ' (GMT) יראה בשעה 11:00 אצלו את ההודעה שזה עתה נוספה לפורום - בעוד שעתיים בשבילו. זה יקרה במקרה שהשרת כשירשום את ההודעה למסד הנתונים, יצרף אליה את שדה "שעת יצירה" לפי שעון ישראל. למרות שגם אם השרת ירשום את השעה לפי שעון גריניץ' עדיין תהיה אי נוחות לדוד לראות שהודעתו צורפה שעתיים לפני שהוא צירף אותה בפועל.
המסקנה המתבקשת היא, שלא משנה לפי איזה איזור זמן תשמור את הנתונים, צריך להמיר אותם לפני ההצגה עבור כל משתמש לגופו. אנו נסקור כאן שני דרכים להתמודדות עם הבעיה ואת חסרונותיהם. בכל הדוגמאות השרת ישמור את הנתונים לפי שעון GMT.
דרך א'
אנו ניצור תופס חדש ונקרא לו userSubmit.aspx. קודם נכתוב את קוד הASP.NET הבא:
<%@ Page Language="VB" AutoEventWireup="false" EnableEventValidation="False"
CodeFile="submitUser.aspx.vb" Inherits="submitUser" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>טוען...</title>
</head>
<body dir="rtl">
<h1>
טוען...</h1>
<form id="form1" runat="server">
<asp:HiddenField ID="offset" runat="server" />
<script type="text/javascript">
var dt = new Date();
document.getElementById(fieldName).value = -(dt.getTimezoneOffset() / 60); // חשב בשעות
document.forms[formName].submit(); // שלח מידע לשרת
</script>
</form>
</body>
</html>
נדגיש שיש להוסיף לדרקטיבה Page %> את הפרמטר "EnableEventValidation="False כדי לאפשר את הSubmit מJavaScript.
אחר כך נפתח את קובץ הCodeBehind במקרה שלנו זה userSubmit.aspx.vb ונוסיף קוד לאירוע Load.
Partial Class submitUser
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not IsPostBack Then
ClientScript.RegisterClientScriptBlock(GetType(Page), "varForm", "var formName = '" + form1.ClientID + "';", True)
ClientScript.RegisterClientScriptBlock(GetType(Page), "varField", "var fieldName = '" + offset.ClientID + "';", True)
Else
Dim offset As String = Request.Form("offset")
' אם תווח השעות נכון
If IsNumeric(offset) AndAlso CInt(offset) >= -12 AndAlso CInt(offset) <= 12 Then
Session("offset") = offset
' חוזרים לפורום
If Request.QueryString("returnurl") <> "" Then
Response.Redirect(Request.QueryString("returnurl"))
End If
End If
' משהו שהתבש חוזרים לדף הראשי
Response.Redirect("~/")
End If
End Sub
End Class
מה שעשינו פה, זה יצרנו טופס (form) עם JavaScript ששולח לשרת את מספר השעות שיש להוסיף לזמן GMT (גריניץ') כדי לקבל את הזמן המקומי, ושמרנו את זה בSession.
כעת, כדי להשתמש במה שעשינו, אנו נוסיף לדף הפורום הפנייה (Response.Redirect) לדף שיצרנו, ונציג למשתמש את השעה שרשומה במסד הנתונים (GMT) + המספר שעות שיושב כרגע בSession. בצורה כזאת:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Session("offset") Is Nothing Then
' נקבל את המידע על הלקוח ונחזור לפה
Response.Redirect("submitUser.aspx?returnurl=" + Request.Url.ToString)
Else
Dim delta As Integer = CInt(Session("offset"))
For Each msg As ForumMessage In ForumDAL.GetAll()
' נחשב את השעה שנציג ללקוח
Dim newTime As Date = msg.SentOn.AddHours(delta)
'... ונוסיף את ההודעה לדף
Next
End If
End Sub
המעלות:
- מעט מאוד קוד JavaScript (טוב לאלרגיים שבינינו).
החסרונות:
- נחוץ רענון נוסף בטעינת הפורום.
- מחייב שימוש בSessions שלעיטים מכביד על האפליקציה כולה. (כמובן שתמיד אפשר להעביר את הנתון הdelta [הפרש זמן] כפרמטר Url ולהפתר מSessions אבל בינינו - זה די מסורבל)
דרך ב'
הרעיון הכללי: אנו לא נתחשב בצד השרת כלל בסביבת הלקוח, אלא נבצע את ההמרה של השעות על ידי JavaScript אחרי רינדור הדף.
בשביל זה, נכתוב קודם את פונקציית הJavaScript המתאימה:
function convertToLocalTime(year,month,day,hour,min,sec) {
var localTime = new Date(year,month,day,hour,min,sec);
var ms = localTime.getTime() - (localTime.getTimezoneOffset() * 60000);
var time = new Date(ms);
return time;
}
function getShortTime(year,month,day,hour,min,sec)
{
var dt = convertToLocalTime(year,month,day,hour,min,sec);
return ZeroPad(dt.getHours()) + ':' + ZeroPad(dt.getMinutes());
}
function getShortDate(year,month,day,hour,min,sec) {
var dt = convertToLocalTime(year,month,day,hour,min,sec);
return ZeroPad(dt.getDate()) + '/' + ZeroPad(dt.getMonth()) + '/' + dt.getFullYear();
}
function getTime(year,month,day,hour,min,sec) {
return convertToLocalTime(year,month,day,hour,min,sec).toLocaleTimeString();
}
function getDate(year,month,day,hour,min,sec) {
return convertToLocalTime(year,month,day,hour,min,sec).toLocaleDateString();
}
function getLong(year,month,day,hour,min,sec) {
return convertToLocalTime(year,month,day,hour,min,sec).toLocaleString();
}
function ZeroPad(val) {
val = val + '';
if (val.length == 1)
val = '0' + val;
return val;
}
לווריאציות השונות של הפונקציה אנו נזדקק אם נרצה לסגנן (Date Formatting) את התאריך בזמן ההצגה ללקוח. (כחלופה, ניתן לשלב כאן את הקוד של Tor2k מקוד פרוג'קט)
ועכשיו את פונקציות הNET. הרלוונטיות:
Public Class LocalTime
Public Shared Function ToLocalShortDate(ByVal val As Date) As String
Return String.Format("<script type='text/javascript'>document.write(getShortDate({0}));</script>", ToParams(val))
End Function
Public Shared Function ToLocalShortTime(ByVal val As Date) As String
Return String.Format("<script type='text/javascript'>document.write(getShortTime({0}));</script>", ToParams(val))
End Function
Public Shared Function ToLocalDate(ByVal val As Date) As String
Return String.Format("<script type='text/javascript'>document.write(getDate({0}));</script>", ToParams(val))
End Function
Public Shared Function ToLocalTime(ByVal val As Date) As String
Return String.Format("<script type='text/javascript'>document.write(getTime({0}));</script>", ToParams(val))
End Function
Public Shared Function ToLocalLong(ByVal val As Date) As String
Return String.Format("<script type='text/javascript'>document.write(getLong({0}));</script>", ToParams(val))
End Function
Private Shared Function ToParams(ByVal val As Date) As String
Return String.Format("{0},{1},{2},{3},{4},{5}", val.Year, val.Month, val.Day, val.Hour, val.Minute, val.Second)
End Function
End Class
אחרי שהקוד מוכן, נוכל לשלבו בASP.NET בשיטת Late Bound Data Expressions. לדוגמה:
<%#LocalTime.ToLocalLong(CType(Container.DataItem, ForumMessage).sentOn)%>
המעלות בגישה זו:
- אין צורך בSessions
- אין התייחסות בקוד צד שרת למיקום הלקוח, וכל הלוגיקה נופלת על הJavaScript.
לסיכום
הכלל בכל גישה אפשרית הוא שהשרת צריך לשמור את כל הזמנים והתאריכים לפי שעון GMT הנגיש דרך: My.Computer.Clock.GmtTime. וא"כ להמירו עבור כל משתמש ומשתמש.
הבעיות הידועות עם הנושא, קשורות לשעוני קיץ הנהוגים במדינות שונות בזמנים שונים (ולא תמיד קבועים), והיות שאין פתרון גורף לבעיה נצטרך לסמוך על יצרני הדפדפנים שיתחשבו בשעון קיץ כפי שמוגדר במערכת ההפעלה מתי שהם מחזירים בJavaScript את התוצאה של getTimezoneOffset. כמובן שכל הטרחה הזאת הייתה נחסכת אילו על פי תקן הHTTP היו מחויבים לשלוח בכל בקשה לשרת כותרת (Header) עם איזור הזמן כפי שמוגדר במערכת של המבקש.
גישות נוספות שקיימות:
- לבקש מהמשתמש בעת ההרשמה לבחור ידנית את איזור הזמן שלו. (איזור מה ??)
- לחשב את הDelta לפי ההפרש המעשי בין השעה שעל מחשב הלקוח לבין השעה שעל השרת. למרות שקרוב לודאי שזה יפתור את הבעיות הקשורות לשעון קיץ, זה לא פתרון מדויק לכן לא נסקור אותו כאן.