יוצא לי ללמד MVVM לעיתים די קרובות, וכמעט תמיד עולה השאלה למה WPF לא תומך בזה “בילט אין”.
הרי CodeBehind ו ViewModel בסופו של יום מאוד דומים, ההבדל הוא רק ש ViewModel הוא מחלקה נפרדת לחלוטין ולפיכך ההפרדה בין הלוגיקה לבין הUI חזקה יותר.
כשמממשים MVVM, ויזו’ואל סטודיו לא יודע על הקישור בין ה View לבין ה ViewModel, ולפיכך אי אפשר לעבור ביניהם בקלות ע”י לחיצה על F7 כמו שאפשר בין ה View ל CodeBehind שלו. בנוסף, היה נחמד אם ויז’ואל סטודיו היה מציג את ה ViewModel “מתחת” ל View, כמו שהוא עושה ל CodeBehind.
בנוסף לכל זה, את ה CodeBehind עצמו אני בכלל לא צריך.. והייתי מעדיף אפילו להעיף אותו בשביל למנוע “כסתוחים” של מפתחים אצלי בצוות. אם אין קובץ CodeBehind, אי אפשר לכתוב CodeBehind!
להפוך את ה CodeBehind ל ViewModel
הרעיון הסתובב לי בראש תקופה לא קצרה, ונראה לי שעליתי על טריק נחמד בשביל לבצע בדיוק את זה. פשוט נהפוך את ה CodeBehind ל ViewModel ונרקוד בשתי החתונות!
כדוגמא, נניח שיש לי View בשם FirstView.
הזאמל המקורי שלו יראה כך:
<UserControl x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
</Grid>
</UserControl>
השורה שמעניינת אותנו היא השורה הזו:
x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
שגורמת לפארסר של זאמל לייצר מחלקה בשם FirstView שיורשת מהמחלקה UserControl. עד כאן בסדר.
אם נעבור לCodeBehind, נראה את הקוד הבא, שכאמור אנחנו לא מעוניינים בו:
namespace ViewModelAsCodeBehindTrick.Views
{ /// <summary>
/// Interaction logic for FirstView.xaml
/// </summary>
public partial class FirstView : UserControl
{ public FirstView()
{ InitializeComponent();
}
}
}
מה שיש כאן זה החלק השני של המחלקה FirstView שמתחבר למחלקה שנוצרה אוטומאטית ע”י זאמל. כל מה שאנחנו צריכים לעשות, זה:
1. לשנות את השם של המחלקה מ FirstView ל FirstViewModel
2. לשנות את ה- namespace ל-ViewModels.
3. להעיף את הקונסטרקטור שקורא ל- InitializeComponent (המתודה לא קיימת ב ViewModel )
4. לרשת מ INotifyPropertyChanged בשביל שה- ViewModel שלנו יהיה מוכן ל DataBinding מאוחר יותר.
בסופו של יום הקובץ יראה כך:
namespace ViewModelAsCodeBehindTrick.ViewsModels
{ /// <summary>
/// Interaction logic for FirstView.xaml
/// </summary>
public class FirstViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
}
לצורך הדוגמא, נוסיף פרופרטי (Property) ל ViewModel ונשים בו סתם טקסט על מנת שנוכל לבדוק שהכל עובד מאוחר יותר כשנחבר הכל. בדוגמא הזו בחרתי להוסיף פרופרטי בשם SomeText, ובקונסטרטור קבעתי את הערך שלו להיות “Hello MVVM”:
public class FirstViewModel : INotifyPropertyChanged
{ private string someText;
public string SomeText
{ get { return someText; } set
{ someText = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SomeText")); }
}
public FirstViewModel()
{ SomeText = "Hello MVVM";
}
public event PropertyChangedEventHandler PropertyChanged;
}
כל מה שנותר עכשיו לעשות זה לחבר את ה View ל ViewModel, ואת זה אפשר לעשות בכל הדרכים הסטנדרטיות. (בדרך כלל אני אשתמש ב ViewModelLocator בשביל לחבר אותם, בדוגמא הזו בחרתי בדרך פשוטה יותר:
<UserControl ... >
<UserControl.DataContext>
<vm:FirstViewModel />
</UserControl.DataContext>
<Grid>
<TextBlock Text="{Binding SomeText}" /> </Grid>
</UserControl>
בנוסף, הוספתי TextBlock שקשור ל SomeText על מנת שאוכל לראות שהכל התחבר כמו שצריך, ואכן אם נסתכל בתצוגה המקדימה של העורך נראה שהקישור עובד כפי שרצינו!

לא כל כך מהר..
הסיפור קצת מסתבך, בגלל שבלי ששמנו לב יצרנו כאן באג בזמן ריצה. אם נשים את ה View שלנו בתוך החלון הראשי, כך:
<Window x:Class="ViewModelAsCodeBehindTrick.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:ViewModelAsCodeBehindTrick.Views"
Title="MainWindow" Height="350" Width="525">
<Grid>
<v:FirstView x:Name="firstView1" />
</Grid>
</Window>
ואז נריץ, לא נראה כלום. הקישור לא עובד משום מה…

וזה למרות שבדיזיין טיים רואים את הטקסט בדיוק כפי שאנחנו מצפים:

אז מה בדיוק קורה פה? למה זה לא עובד בזמן ריצה למרות שזה *כן* עובד זמן עיצוב?!
האינסטינקט המיידי של לבדוק את חלון ה- output ולראות אם יש שגיאות Binding לא יעזור לנו כאן. זו בעיה אחרת לחלוטין…
(שווה לקחת כמה שניות ולחשוב על זה ולנסות לעלות על זה לבד..
)
.
.
.
.
.
.
.
.
.
.
להבין את תהליך פרסור הזמאל
אז ככה. מקובץ הזאמל נוצרים שני קבצים בזמן הקימפול:
1. FirstView.g.cs שבו יושבת המחלקה FirstView. המחלקה הזו טוענת את הקובץ השני -
2. FirstView.Baml שזהו קובץ הזאמל שלנו אחרי סוג של קמפול. (למעשה אחרי Tokenization – פרסור הקובץ כך שהטעינה תתבצע מחר יותר בזמן ריצה)
הטעינה והחיבור של שני הקבצים מתבצעות במתודה InitializeComponent שנמצאת ב FirstView.g.cs. רק.. שעכשיו שמחקנו את ה- CodeBehind אף אחד לא קורה לה. לפיכך, ה View שלנו פשוט לא טוען את קובץ הBaml ולפיכך הוא נשאר ריק. מה שמשעשע כאן זה שויז’ואל סטודיו בזמן העיצוב (Design Time) כן קורא למתודה הזו באופן אוטומאטי, וזו הסיבה שבזמן העיצוב כן ראינו שזה עובד.
כל מה שאנחנו צריכים לעשות בשביל שזה יעבוד, זה לודא שהמתודה הזו נקראת בזמן ריצה… אבל איך?
יצירת UserControl שקורא אוטומאטית ל InitializeComponent
הפתרון שאני מצאתי לזה הוא טריק די נחמד. במקום שהיוזר קונטרול יירש מ UserControl, נגרום לו לרשת ממחלקה חדשה בשם ViewBase.
ניצור מחלקה חדשה בשם ViewBase, שיורשת מ UserControl:
public class ViewBase : UserControl
{}
ובמקום שהView שלנו יירש ישירות מ UserControl, ניתן לו לרשת מ ViewBase:
<v:ViewBase x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:ViewModelAsCodeBehindTrick.ViewsModels"
xmlns:v="clr-namespace:ViewModelAsCodeBehindTrick.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<vm:FirstViewModel />
</UserControl.DataContext>
<Grid>
<TextBlock Text="{Binding SomeText}" /> </Grid>
</v:ViewBase>
(כשמשנים את UserControl ל v:ViewBase ויז’ואל סטודיו לא נותן intellisense. זה בסדר. אנחנו עושים משהו שויז’ואל סטודיו לא מצפה שנעשה, אבל זה תקין).
כל מה שנותר לנו כעת הוא לגרום למחלקה שלנו לקרוא ל InitializeComponent בקונסטרקטור.
הבעיה היא שאם פשוט נקרא לה נגלה שהיא לא קיימת.. המתודה תיווצר רק מאוחר יותר על ידי כשהזאמל יקומפל, בזמן ריצה…

אז אנחנו צריכים לקרוא למתודה בזמן ריצה. המתודה קיימת בזמן ריצה, אבל לא קיימת בזמן העיצוב, ולכן אי אפשר לקרוא לה כי זה לא יתקמפל.
שום דבר שקצת רפלקשן לא יפתור 
this.GetType().GetMethod("InitializeComponent").Invoke(this, null);
אם נריץ עכשיו את האפליקציה נראה שזה אכן עובד!
הבעיה היא שאם נחזור לויז’ואל סטודיו נראה שעכשיו זה עובד בזמן ריצה, אבל… דפקנו את זמן העיצוב..

מה שקורה עכשיו זה שאנחנו מנסים לקרוא למתודה InitilizeComponent בזמן העיצוב, שזה בלתי אפשרי.
את זה כבר יהיה קל לפתור – פשוט נודא שהקריאה הזו תתבצע אך ורק אם אנחנו לא בזמן העיצוב:
public class ViewBase : UserControl
{ public ViewBase()
{ if (!DesignerProperties.GetIsInDesignMode(this))
this.GetType().GetMethod("InitializeComponent").Invoke(this, null); }
}
ועכשיו זה יעבוד מושלם גם בזמן ריצה וגם בזמן עיצוב!
האם זה שווה את המאמץ?
בשורה התחתונה, אחרי שכתבנו את הViewBase פעם אחת, יהיה נורא קל להשתמש בזה כמה שנרצה, כשאנחנו מקבלים כמה פיצ’רים מאוד נחמדים:
1. קיבלנו ViewModel שנמצא בsolution explorer מתחת לview. מאוד עוזר לטעמי.
2. אין יותר CodeBehind מיותר!
3. אם נמצאים View ורוצים לעבור ל ViewModel, כל מה שצריך לעשות זה פשוט ללחוץ F7!
(לצערי בכיוון ההפוך זה לא עובד, אבל זה עדיין לא מעט)
לפי דעתי זה בהחלט שווה את המאמץ, אבל גם אם לא, זה עדיין טריק ממש נחמד, לא? 
שווה להוריד את הקוד המלא ולשחק עם זה. אשמח מאוד לשמוע חוות דעת – האם זה שימושי? האם זה באמת עוזר?
את הקוד המלא אפשר להוריד מכאן