לממש MVVM הרבה יותר קל ממה שנשמע. משום מה יצא לDesign Pattern הזה שם של Design Pattern מורכב, ולפי דעתי זו סתם חוסר הבנה. הDesign Pattern במהותו פשוט עד טריוויאלי.
בפוסט הקודם דיברתי על הסיבות לממש MVVM, ועל הפואנטה שעומדת מאחורי הDesignPattern הזה. העליתי גם שאלה – מה גורם להפרדה של XAML ו Code Behind לא להוות הפרדה מספיק טובה.
אז האמת שההפרדה של XAML ו CodeBehind כבר לא ממש רעה, רק שהבעיה היא שהם לא ממש מופרדים – הם למעשה מהווים ביחד מחלקה אחת, ולכן שינויים באחד מהם לעיתים מאוד קרובות "ישברו" את השני. (ה Coupling כאן גבוה, ואת זה נרצה לשנות).
כל הרעיון מאחורי MVVM זה פשוט לקחת את הCodeBehind ולהוציא אותו למחלקה נוספת, שנקרא לה ViewModel, וכן תהייה לנו הפרדה חזקה.ה View יהיה הXAML, והלוגיקה תשב במחלקה נפרדת לחלוטין.
בנוסף, נקבע שאסור לView להכיר את הViewModel, ואסור לViewModel להכיר את הView. זאת אומרת שלא תהייה לנו אף פעם גישה מאחד לשני בצורה ישירה, וכך ההפרדה תשאר חזקה – כך שינויים בשכבה אחת לא ישברו את השכבה השניה אף פעם!
הבעיה היחידה היא כמובן איך מתבצעת שליטה של שכבת הלוגיקה (ה ViewModel) על שכבת התצוגה (ה View) אם הן שתי מחלקות נפרדות, ש"לא מכירות" אחת את השניה? כאן נכנסים שני פיצ'רים מאוד חזקים של WPF ו Silverlight – Binding – שעביר מידע בין הViewModel לבין הView, ו Commands, שיעבירו פקודות מהView לViewModel. זה הרבה יותר פשוט ממה שזה נשמע.
בשביל שהמחלקות יכירו אחת את השניה בצורה "חלשה", נקבע את מחלקת הViewModel להיות הDataContext של מחלקת הView. התקשורת בין שתי המחלקות תעבור לפיכך, ע"י שימוש בBinding וCommands.
אז תכלס, איך מממשים את זה?
כדוגמא ניקח את המסך הבא, שמראה כפתור ו ListBox. בלחיצה על הכפתור, ה ListBox יראה לנו רשימה של מוצרים:

לממש את זה בדרך הרגילה של XAML ו CodeBehind די פשוט.ניצור UserControl חדש שיכיל את כל הUI, ונקרא לו ListOfProducts.
ה XAML שלנו יראה בערך כך:
1: <UserControl x:Class="WpfApplication21.ListOfProducts"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
4: <Grid>
5: <Button Content="Show Products" VerticalAlignment="Top" Click="Button_Click" />
6: <ListBox Margin="0,23,0,0" DisplayMemberPath="Name" x:Name="MyListBox">
7: </ListBox>
8: </Grid>
9: </UserControl>
והCode Behind היה נראה כך:
1: public partial class ListOfProducts : UserControl
2: {
3: public ListOfProducts()
4: {
5: InitializeComponent();
6: }
7:
8: private void Button_Click(object sender, RoutedEventArgs e)
9: {
10: DummyService service = new DummyService();
11: var products = service.GetAllProducts();
12:
13: MyListBox.ItemsSource = products;
14: }
15: }
ניקח את המסך הזה כדוגמא, ונהפוך אותו מXAML ו CodeBehind, ל View ו ViewModel:
כרגע ישנה תקשורת בין הXAML ל CodeBehind ובין הCodeBehind לבין הXAML.
בשביל להעביר את האפליקציה הזו לMVVM, נבצע שלושה דברים: ניצור מחלקת ViewModel, נרשום את הViewModel כ DataContext של ה View, ונקשר את הכל ע"י Binding.
1. יצירת ה ViewModel
ניצור מחלקה חדשה, ונקרא לה ListOfProductsViewModel. (כסוג של סטנדרט, מחלקות ViewModel מסתיימות בViewModel, בעוד מחלקות של View מסתיימות בView.)
המחלקה הזו צריכה למעשה להחליף את הCodeBehind. לכן היא צריכה להכיל שני חלקים חשובים-
1. לחשוף Property מסוג List<Product> - רשימת המוצרים - בשביל שהView יוכל להציג.
2. לחשוף Command שיאפשר לView להרים כאשר הכפתור נלחץ:
1: // The ViewModel class. Implements INotifyPropertyChanged to help DataBinding.
2: public class ListOfProductsViewModel : INotifyPropertyChanged
3: {
4: // The Products property.
5: private ObservableCollection<Product> products;
6: public ObservableCollection<Product> Products
7: {
8: get { return products; }
9: set
10: {
11: products = value;
12: if (PropertyChanged != null)
13: PropertyChanged(this, new PropertyChangedEventArgs("Products"));
14: }
15: }
16:
17: // Property for the Command. (RelayCommand is a helper class that wraps ICommand).
18: public RelayCommand ShowProductsCommand { get; set; }
19:
20: // Reference to the dummy service.
21: DummyService service;
22:
23: // ctor
24: public ListOfProductsViewModel()
25: {
26: // Create a new product service.
27: service = new DummyService();
28:
29: // initialize the command
30: ShowProductsCommand = new RelayCommand(ShowProducts);
31: }
32:
33: void ShowProducts()
34: {
35: var productList = service.GetAllProducts();
36:
37: this.Products = new ObservableCollection<Product>(productList);
38: }
39:
40:
41:
42: public event PropertyChangedEventHandler PropertyChanged;
43: }
הסבר:
1. מחלקת הViewModel מכינה את המידע (הProperties) ואת הפעולות (Commands) עבור מחלקת הView (הUserControl שיציג את ה UI ).
2. מחלקת הViewModel לא מכילה שום קישור לView. זו נקודה מאוד חשובה, אולי הכי חשובה בכל הסיפור הזה. זו תמצית ההפרדה בMVVM, ונראה מאוחר יותר כמה כוח זה יתן לנו. מכאן נובע כל הכוח וכל היכולות החזקות של MVVM.
3. אפשר להסתכל על הViewModel כעל CodeBehind שלא מכיל שום קישור ישיר לXAML. הViewModel בדר"כ יהווה את כל ה"תוכן" של האפליקציה, רק בלי להציג אותה. כל המידע, הלוגיקה, והמצב (State) של האפליקציה ימצא ב ViewModel. הView רק יציג, ולא שום דבר מעבר לזה.
4. הCommand שאנו חושפים הוא מסוג RelayCommand שזו פשוט מחלקה שמממשת את ICommand. המחלקה הזו מגיע מספריית עזר בשם MVVMLight, אבל היינו יכולים ליצור את זה גם בעצמינו. עוד על כך בהמשך.
2. קישור הViewModel לView
עכשיו שיש לנו מחלקה שמחליפה את הCodeBehind, אפשר למחוק משם את כל הקוד. תחת MVVM אנחנו תמיד נשאף למצב בו אין לנו בCodeBehind שום קוד בכלל. כרגע, כל מה שנרצה לשים ב Code Behind זה אך ורק את הקישור לViewModel:
1: public partial class ListOfProductsView : UserControl
2: {
3: public ListOfProductsView()
4: {
5: InitializeComponent();
6:
7: this.DataContext = new ListOfProductsViewModel();
8: }
9: }
בהמשך נראה דרכים הרבה יותר חכמות לחבר את הViewModel ל View, ללא שימוש בCodeBehind בכלל. כרגע זה יספיק לנו (בהמשך נשתמש בViewModelLocator, אבל זה כבר יחכה לפוסט הבא)
3. יצירת הBinding בתוך הView
כל מה שנותר לנו לעשות זה לחבר את הBinding בתוך הView. לפיכך נערוך את הXAML כך שיראה כך:
1: <UserControl ...>
2: <Grid>
3: <Button Content="Show Products" VerticalAlignment="Top" Command="{Binding ShowProductsCommand}" />
4: <ListBox Margin="0,23,0,0" DisplayMemberPath="Name" x:Name="MyListBox" ItemsSource="{Binding Products}">
5:
6: </ListBox>
7: </Grid>
8: </UserControl>
וזהו. יש לנו את אפליקציית הMVVM הפשוטה ביותר בעולם. זה כל ה Design Pattern, וכאמור הוא די פשוט.
אז למה יצא לMVVM שם של ארכיטקטורה מורכבת? מה הקושי ב MVVM ?
האפליקציה שבנינו כאן היא די פשוטה, ויש לא מעט סיטואציות בהן יהיה לנו קשה לבצע הכל ע"י סה"כ Binding ו Commands. הסיטואציות הבעייתיות הקלאסיות הן בדר"כ:
1. מימוש UX מתוחכם, בו יהיה לנו קשה לבצע הכל מהViewModel . כדוגמא, מימוש של Drag and Drop, וכו'.
2. בעייתיות בCommands – Commands בWPF וSilverlight זה פיצ'ר טיפה בעייתי. יש מעט מאוד קונטרולים שיודעים להרים Command, וגם הם רק לClick . איך אני תופס אירוע כמו MouseMove או TextChanged ב MVVM ?
3. בעיות ניווט בין Views שונים – בעיית הניווט (Navigation) היא בעייה תמיד כשכותבים אפליקציות, וב MVVM היא מוחרפת. מעבר בין מסך למסך זה משהו שהיינו רוצים לשלוט עליו בViewModel , אבל לViewModel אסור לגעת ישירות בView ואי אפשר לעשות את זה עם Binding בלבד... אז איך מבצעים Navigation?
להבדיל מלפני שנה-שנתיים – כשMVVM התחיל להיות פופולארי לראשונה – היום הארכיטקטורה הזו בוגרת לחלוטין, ויש פתרונות טובים מאוד לכל הבעיות הללו.
על כך, בפוסטים הבאים.
את הקוד אפשר להוריד מכאן