מה הסוד של M-V-VM

2 בינואר 2011

תגיות: ,
תגובה אחת

שנת 2010 בתחום ה-UI היתה שנת ה-MVVM . בהרבה חברות שעבדתי בשנת 2010 בחרו בפה אחד לעבוד ע"פ ה-Pattern של M-V-VM. למה? אמרו לי שיותר קל לכתוב בדיקות, במה זה מתבטא? ומה קרה שנטשנו את ה- MVC או את ה-MVP ? בפוסט זה אני אנסה להסביר את הסיבות למה אני בחרתי ב-MVVM ומה חסר.

טענה: ה-MVVM עוזר לנו לכתוב קוד שעושה הפרדה בין חלקי ה-UI ללוגיקה של המסך.

כאשר מסתכלים על המסך הפשוט הזה מגלים שיש המון צורות לכתוב אותו.

image

גירסת ה-"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 ולא למידע ויזואלי.

image

ע"י כתיבה של מחלקה נפרדת מה-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.

clip_image002

ה-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="G
etUserTask"
   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, האם פגענו ביכולות הבדיקות שלנו?

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

תגובה אחת

  1. amit kuzi8 בינואר 2011 ב 20:40

    השאלה היא … למה אנו מתעקשים לממש V כ USERCONTROL ?
    ולא פשוט לממש את ה VIEW כ DATATEMPLATE … ואז כל CONTENT CONTROL או ITEMS CONTROL שקבל את ה VM כ DATA
    יוכל לטעות אותו אוטומטית ולהציג אותו בקלות ?????

    הגב