שנת 2010 בתחום ה-UI היתה שנת ה-MVVM . בהרבה חברות שעבדתי בשנת 2010 בחרו בפה אחד לעבוד ע"פ ה-Pattern של M-V-VM. למה? אמרו לי שיותר קל לכתוב בדיקות, במה זה מתבטא? ומה קרה שנטשנו את ה- MVC או את ה-MVP ? בפוסט זה אני אנסה להסביר את הסיבות למה אני בחרתי ב-MVVM ומה חסר.
טענה: ה-MVVM עוזר לנו לכתוב קוד שעושה הפרדה בין חלקי ה-UI ללוגיקה של המסך.
כאשר מסתכלים על המסך הפשוט הזה מגלים שיש המון צורות לכתוב אותו.
גירסת ה-"VB" ( לחיצה כפולה על פקד ה-Send )
public partial class MainWindow : Window
{
...
private void btnSendMail_Click(object sender, RoutedEventArgs e)
{
string to = tbTo.Text;
string subject = tbSubject.Text;
string msg = tbMsg.Text;
proxy.SendMail(to, subject, msg);
}
}
הקוד הזה מאוד אינטואינטיבי, מאוד פשוט, אך מאוד בעייתי. יש פה קשר בין הפקד ה-UI לבין הלוגיקה.
אם מחר אני ארצה לעבור מפקד TextBox אחד לפקד TextBox גירסה 2, או פקד אחר שאין לו את ה-Property של ה-Text. אני יכול לקבל התנהגות שונה לגמרי.
איך אפשר לשפר את זה?
public partial class MainWindow : Window
{
public string To { get; set; } public string Subject { get; set; }
public string Message { get; set; }
private void btnSendMail_Click(object sender, RoutedEventArgs e)
{
proxy.SendMail(To, Subject, Message);
}
}
בשינוי הקטן שבו כל הקוד עובד מול ה- Properties ולא מול הפקדים אני מנתק את התלות שלי ב-UI. נשמע כל כך פשוט, אפילו טריוויאלי... אז למה כל פעם שאני מגיע לפרויקט גדול אני רואה בקוד, בעיקר בקוד של החלונות וה-UserControls נגיעה בפקדים במספר מקומות ולא עבודה רק מול ה- Properties? למשל פניה ל- ListBox.SelectedItem. לפי דעתי ,הסיבה לכך היא, שאנחנו כותבים קוד בחלון וכל ה- Properties של הפקדים נגישים לנו, אז אנחנו מתעצלים לכתוב שוב את ה- Properties בחלון ולעבוד מולם. בנוסף, צריך לעשות הפרדה בין מידע לוגי שקיים בפקד למידע ויזואלי. רק למידע הלוגי צריך לכתוב Property ולא למידע ויזואלי.
ע"י כתיבה של מחלקה נפרדת מה-View שנקרה לה ViewModel אנחנו מקבלים את היתרונות הבאים:
1. ל-VM אסור להכיר את ה-View, מה שישמור אלינו מלגשת ישירות לפקדים, כלומר אנחנו חייבים להגדיר Property לכל מידע שאנחנו רוצים לעבוד איתו.
2. ה-VM יכול לרשת מאיזה מחלקה שהוא רוצה.
3. אני ממליץ לכתוב קודם את VM ורק אחר כך את ה-View, זה מדגיש את הניתוק בין ה-View ל-VM.
4. החיבור בין הפקדים ב-View ל-Properties ב-VM מתבצע ע"י DataBinding.
איך מחברים את המתודה שב-VM SendMail לפקד הכפתור שב-View?
כאן אנחנו נשתמש ב-Command ,כלומר ,אנחנו נגדיר Property מסוג ICommand בשם Send שעליו הכפתור יעשה DataBinding.
<Button
Name="btnSendMail"
Command="{Binding Send}"
CommandParameter="?" .../>
בשיטת העבודה לעיל , יש מספר שאלות שאנחנו צריכים לתת עליהם את הדעת:
1. למה לא לעבוד עם RoutedCommand ו- CommandBinding?
2. CommandParameter נותן לנו להעביר רק Parameter אחד, האם זה מספיק?
3. למה לא לעבוד עם Action?
תשובות:
1. RoutedCommand ו- CommandBinding, היה כבר בגירסה הראשונה של WPF לפני שה-MVVM התחיל לפרוח. ה- RoutedCommand יוצר קשר בין רכיב ה-UI המפעיל את הפקודה לבין רכיב ה-UI התופס את הפקודה. למשל
פקודת "חיתוך" ("Cut") היא מסוג RoutedCommand, אין בה שום מימוש איך לעשות את החיתוך, אבל עם ה- RoutedCommand יתפס ע"י TextBox למשל הוא יודע איך לבצע את החיתוך, כל פקד מגדיר את החיתוך פנימית אצלו.
<StackPanel Orientation="Vertical">
<TextBox Name="textBox1">
<TextBox.CommandBindings>
<CommandBinding Command="Cut" />
</TextBox.CommandBindings>
</TextBox>
<Button
Content="Cut"
Command="Cut"
CommandTarget="{Binding ElementName=textBox1}" />
</StackPanel>
הקישור בין שני הרכיבים יוצר תלות בין שניהם ואנחנו רוצים לכתוב קוד לוגי בלי להכיר את ה-UI. לכן בשיטת ה-MVVM אנחנו צריכם להחזיק את Text של TextBox ב-Property ב-VM ולכתוב Command ב-VM שיודע לדמות את פעולת ה-Cut.
ה-Property של Text ב- VMמחובר ב-DataBinding ל- Propertyה-Text של פקד ה-TextBox.
לחיצה על הכפתור תפעיל את הפקודה של החיתוך על ה-VM, כלומר תאפס את Text על ה-VM ובגלל ה-DataBinding יאפס גם את TextBox ותשים את הטקסט בClipboard- או במשתנה זמני.
מה קיבלנו? שיש לנו ב-VM פעולת חיתוך עובדת וניתנת לבדיקה בלי קשר ל-UI (אבל עבדנו קשה בשביל זה ).
2. CommandParameter נותן לנו להעביר רק Parameter אחד, האם זה מספיק? כן. אם עובדים MVVM אז אנחנו לא צריכים להעביר כלום ב- CommandParameterכי הנתון שאנחנו רוצים להעביר ב-DataBinding ל-VM צריך להיות מקושר מראש ל-VM. דוגמא "רעה":
<ComboBox Name="Users" />
<Button
Content="Get Tasks"
Command="GetUserTask"
CommandParameter="{Binding ElementName=Users, Path=SelectedItem}" />
הקשר שנוצר בין ה- CommandParameterשל הכפתור ל-ComboBox לא תקין, צריך שב-VM יהיה Property שמחזיק את ה- SelectedItem של ה- ComboBoxולכן לא יהיה צורך להעביר אותו ע"י ה-.CommandParameter
דוגמא "טובה":
<ComboBox Name="Users"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}"/>
<Button
Content="Get Task"
Command="GetUserTask" />
ע"פ שיטה זו אי אפשר לבנות Command כיחידה נפרדת שלא מכירה את VM, כי אחרת חייבים להעביר לה CommandParameter. מסיבה זו אנחנו נעשה שימוש ב- DelegateCommand. זה החלק שאני לא אוהב כי זה יוצר קוד צנרת. הצנרת באה לידי כך שחוץ מהמתודות של ה- CanExecuteו- Execute צריך לכתוב גם Property מסוג DelegateCommand וגם ליצור מופע של המחלקה עם הפרמטרים של המתודות CanExecute ו- Execute.
(לא אסון אבל מעצבן).
3. מתי משתמשים ב- Action?
Action משמש כ"דבק" בין הפקד לפעולה מסוימת. אם הפעולה לא מוגדרת ב-VM, אז הכנסנו לוגיקה ל-UI דבר שאנחנו מנסים להימנע ממנו. לכן שימוש ב- CallMethodAction או ב- InvokeCommandAction שמחברים את ה-VM עם פקד זה בסדר גמור. צריך לזכור שעבודה עם Actions אלו נותנת לי שליטה להחליט מה יפעיל אותם, דבר שב-Command אין לי.
<ComboBox ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="DropDownClosed">
<i:InvokeCommandAction Command="{Binding GetUserTask}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
<ListBox ItemsSource="{Binding UserTasks}" />
כל ה-Binding מתבצע מול ה-VM, ה-Action רק מפעיל את ה-Command או את המתודה, שב-VM.
סיכום:
עבודה ב-MVVM מאפשרת לנו לכתוב לוגיקה של המסך בלי תלות במסך עצמו, קרי פיתוח ה-VM לפני פיתוח ה-V. פיתוח כזה גם יאפשר לנו לכתוב בדיקות על VM בלי קשר ל-V. בשל סיבות אלו נחשב ובצדק ה-MVVM יותר נוח לבדיקות.
שאלה לסיכום: אם ניקח את הקוד של ה-VM ( נניח שהוא לא יורש מאף מחלקה ) ונדביק אותו לתוך ה-V, האם פגענו ביכולות הבדיקות שלנו?