DCSIMG
April 2012 - Posts - אלעד כץ | Elad Katz
Sign in | Join | Help

אלעד כץ | Elad Katz

לגו של גדולים

April 2012 - Posts

עשר הטעויות הנפוצות ביותר ב MVVM שכמעט כל אחד נופל בהן

פורסם בתאריך Apr 29 2012, 10:54 AM על ידי eladkatz

קיבלתי לא מעט תגובות על הפוסט הקודם שלי – הפיכת ה Code behind ל ViewModel ב WPF – כשחלקן גרמו לי לחשוב עוד פעם כמה חסר תיעוד מסודר של MVVM. יש המון חומר כתוב באינטרנט, אבל רובו המוחלט לא מסודר, לא מדוייק, ואפילו לפעמים ממש מטעה.
המצב עד כדי כך מורכב שכמעט לא יוצא לי לייעץ בחברה בה לא נופלים לטעות כזו או אחרת, היות ולא מעט טעויות השתרשו כפתרונות לגיטימיים במהלך השנתיים האחרונות.

שתי תגובות קלאסיות שקיבלתי יותר מפעם אחת היו:
1. “ב MVVM לפעמים ה ViewModel משרת כמה View-ים, והדרך שהצגת בפוסט מאפשרת רק יחס של 1:1”
2. “בפוסט למעשה הורדת את ה- Code Behind לחלוטין, ויש מקרים בהם כן נצטרך code-behind!”

שתי התגובות הללו מבוססות על טעויות נפוצות ב-MVVM שחשוב מאוד להמנע מהן.

להסביר לעומק כל טעות, ומה בעייתי בה זה סיפור לא פשוט בכלל.

בקרוב אעלה סדרת פוסטים מאוד רחבה על ארכיטקטורת UI בה אנסה לסדר חומר על כל הנושאים המרכזיים, כש MVVM זו רק ההתחלה (בשלב הזה התוכנית היא MVVM, Prism, Inversion of Control, SOLID principles in UI, Plugin Architecture, אבל זה עוד כפוף לשינויים..). היותר וסדרת פוסטים כזו זה לא משהו פשוט לכתוב (או לפחות יקח לי לא מעט זמן) חשבתי שלבינתיים רשימת טעויות ואינדיקציות יכול כבר לעזור בלא מעט פרוייקטים.

solid_thumb

 

עשר הטעויות הכי נפוצות ב- MVVM

1. שימוש ב ViewModel אחד לשרת כמה View-ים שונים. (הפרה של עקרון SRP י)
2. שימוש ב code-behind, ברמה כל שהיא. (מדרון חלקלק)
3. שימוש ברכיבי Pub/sub כמו Messenger או EventAggregator בשביל תקשורת בין ViewModels שונים בצורה חופשית מדי (אחת הטעויות הנפוצות ביותר) SRP
4. יצירת View בשביל קונטרול ריוזאבילי (reusable control)י
5. יצירת היררכיית מחלקות גדולה ב ViewModel (עקרונית, תמיד עדיף שה ViewModel לא יירש משום מחלקה אחרת עד כמה שאפשר)
6. שימוש בירושה בשביל reuse של התנהגות. (ירושה היא כמעט תמיד רק בשביל פולי-מורפיזם)
7. יצירת צימוד גבוה בין ViewModel-ים שונים (דומה ל 3#) ע”י החזקת רפרנס מ ViewModel אחד לשני.(כמעט תמיד טעות)
8. החזרת רפרנס בצורה ישירה ל ViewModel מתוך שכבת ה Services. (יצירת service-ים שלמעשה מנהלים את ה ViewModel-ים)
9. כתיבה ב-MVVM ללא הקפדה על בלנדאביליות (Blendability )י
10. הכנסת קוד של View לשכבת ה ViewModel. (הפרה של SRP)

 

כל אחת מן הטעויות הללו יכולות בקלות להפוך את האפליקציה שלנו לקשה מאוד לתחזוקה, ואת MVVM למיותר. אם אתם מזהים שאתם מבצעים את אחת מהטעויות הנ”ל שווה לעצור ולראות אם צריך לתקן את זה.

בנוסף, לפעמים קשה לזהות שיש טעויות במערכת, במיוחד אם זו מערכת גדולה שלא כל המפתחים מבינים את כל החלקים בה.
בשביל מקרים כאלו אפשר תמיד להשתמש באינדיקציות כלליות לכך שכנראה יש טעות ארכיטקטונית:

אינדיקציות לכך שיכול להיות שיש לנו טעות ארכיטקטונית/ניהולית בפרוייקט

1. יצירת ViewModel-ים עם יותר מ-500 שורות קוד.
2. מסך ה designer ב VS לא מראה שום דבר שימושי.
3. כתיבת פיצ’ר חדש בתוכנה לוקחת יותר מפי שניים זמן מאשר זה היה לוקח בהתחלה. (אסור להתפשר על זה!!)
4. יכול להיות מצב בו מפתח תקוע בעבודה בגלל שקוד של מפתח אחר לא עובד או לא גמור.
5. במידה ולא כל המפתחים מבינים את התשתיות.
6. במידה והתשתיות גדולות (עקרון KISS מאוד חשוב בתשתיות), ולכן יכול בכלל להווצר מצב ש-#5 יקרה.
7. לוקח יותר מ-20 שניות מהזמן שמתכנת מריץ את האפליקציה עד שהוא יכול לראות את הפיצ’ר שעליו הוא עובד באותו זמן (אחד הדברים החשובים ביותר, ולצערי משהו שאני רואה כמעט בכל מקום)
8. מחלקות עם השם Manager בתוכן (לעיתים קרובות אינדיקציה ליצירת god-objects, אם כי לא תמיד)

כאמור, אנסה בהמשך לתת הסבר מפורט על כל אחד מהנ”ל, אבל בלי להסביר בכלל אי אפשר, ולכן נתרכז בשתי התגובות המרכזיות על הפוסט הקודם..

 

1. בMVVM כל ViewModel ישרת View אחד, ו View אחד בלבד!

המטרה של ViewModel היא להחזיק אחריות מסוימת על חלק מסוים באפליקציה. האחריות הזו היא אחריות מאוד ספציפיות ולכן ה ViewModel לא אמור להתאים לכמה View-ים שונים. למעשה, יש כאן עקרון רחב יותר שבא לידי ביטוי. SRP – Single Responsibility Principle יכול להתנגש עם עקרון DRY – Don’t Repeat Yourself. ארחיב על כך בפוסטים הבאים, אך הנקודה המשמעותית היא שכל מחלקה אמורה להיות או עם אחריות מסוימת , ואז המחלקה לא תהיה ריוזאבילית (reusable), או שמחלקה כן תהייה ריוזאבילית, ואז לא תהייה לא אחריות ספציפית באפליציה. (כדוגמא קוד של קונטרול).

2. בMVVM אין צורך להשתמש אף פעם ב CodeBehind

MVVM מצריך לא מעט קוד של UI, אבל אף פעם לא כדאי לשים את הקוד ב CodeBehind אלא ב Behaviors או Control-ים. CodeBehind הוא מקום מאוד בעייתי היות וקל מאוד לשים שם גם לוגיקה ממש ולא רק UI, ותוך כדי צימוד גבוה ל UI (וצימוד בין UI לבין לוגיקה היא מטרת ה Design Pattern מלכתחילה).

איך ליצור Binding ללא כתיבת קוד–ע”י שימוש בBinding Wizard של VS2010

פורסם בתאריך Apr 25 2012, 06:22 PM על ידי eladkatz

שאלה שנשאלה בפורום WPF –

ש: כשכותבים Binding ב WPF יש אינטיליסנס (השלמת כתיבה) חלקית בלבד. האם יש דרך שויז’ואל סטודיו ישלים את מה שאנחנו כותבים?
ת: האמת היא שיש - החל מויז'ואל סטודיו 2010 - והדרך הכי טובה לראות איך זה עובד זה ע"י הדגמה של היכולת (מומלץ לראות באיכות גבוהה - 720p):


איך לגרום ל WPF להתנהג כאילו ש MVVM באמת נתמך מהקופסא - להחליף בין ה Code Behind ל ViewModel !

פורסם בתאריך Apr 24 2012, 07:07 PM על ידי eladkatz

יוצא לי ללמד 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 על מנת שאוכל לראות שהכל התחבר כמו שצריך, ואכן אם נסתכל בתצוגה המקדימה של העורך נראה שהקישור עובד כפי שרצינו!

image

 

 

לא כל כך מהר..

הסיפור קצת מסתבך, בגלל שבלי ששמנו לב יצרנו כאן באג בזמן ריצה. אם נשים את ה 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>

 

ואז נריץ, לא נראה כלום. הקישור לא עובד משום מה…

image

 

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

image

אז מה בדיוק קורה פה? למה זה לא עובד בזמן ריצה למרות שזה *כן* עובד זמן עיצוב?!
האינסטינקט המיידי של לבדוק את חלון ה- output ולראות אם יש שגיאות Binding לא יעזור לנו כאן. זו בעיה אחרת לחלוטין…

(שווה לקחת כמה שניות ולחשוב על זה ולנסות לעלות על זה לבד.. Smile )

.

.

.

.

.

.

.

.

.

.

 

להבין את תהליך פרסור הזמאל

אז ככה. מקובץ הזאמל נוצרים שני קבצים בזמן הקימפול:

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 בקונסטרקטור.
הבעיה היא שאם פשוט נקרא לה נגלה שהיא לא קיימת.. המתודה תיווצר רק מאוחר יותר על ידי כשהזאמל יקומפל, בזמן ריצה…

image

אז אנחנו צריכים לקרוא למתודה בזמן ריצה. המתודה קיימת בזמן ריצה, אבל לא קיימת בזמן העיצוב, ולכן אי אפשר לקרוא לה כי זה לא יתקמפל.

שום דבר שקצת רפלקשן לא יפתור Smile

 

this.GetType().GetMethod("InitializeComponent").Invoke(this, null);

 

אם נריץ עכשיו את האפליקציה נראה שזה אכן עובד!

image 

 

הבעיה היא שאם נחזור לויז’ואל סטודיו נראה שעכשיו זה עובד בזמן ריצה, אבל… דפקנו את זמן העיצוב..

 

image

מה שקורה עכשיו זה שאנחנו מנסים לקרוא למתודה 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!
(לצערי בכיוון ההפוך זה לא עובד, אבל זה עדיין לא מעט)

 

לפי דעתי זה בהחלט שווה את המאמץ, אבל גם אם לא, זה עדיין טריק ממש נחמד, לא? Smile

 

שווה להוריד את הקוד המלא ולשחק עם זה. אשמח מאוד לשמוע חוות דעת – האם זה שימושי? האם זה באמת עוזר?

את הקוד המלא אפשר להוריד מכאן

טיפ WPF - איך לגרום ל ToggleButtons להתנהג כמו קבוצה? איך לממש את זה ב MVVM ?

פורסם בתאריך Apr 04 2012, 09:01 PM על ידי eladkatz

לקוח שאל אותי לאחרונה איך אפשר להשתמש ב Toggle Button כך שרק אחד יהיה בחור בכל רגע נתון, ושבמידה ולוחצים על אחר אז הכפתור הקודם לא יהיה בחור יותר, זה בערך המצב אליו הוא רצה להגיע:

SNAGHTML593b808

רק שבמימוש הנאיבי הראשוני שום דבר לא מונע מהכפתורים להיות לחוצים כמה ביחד:

<UniformGrid Rows="6" Columns="2" HorizontalAlignment="Left" >
    <UniformGrid.Resources>
        <Style TargetType="ToggleButton">
            <Setter Property="Margin" Value="2" />
            <Setter Property="Padding" Value="10,5" />
        </Style>
    </UniformGrid.Resources>
    <ToggleButton Content="tool1" />
    <ToggleButton Content="tool2" />
    <ToggleButton Content="tool3" />
    <ToggleButton Content="tool4" />
    <ToggleButton Content="tool5" />
    <ToggleButton Content="tool6" />
</UniformGrid>

 

SNAGHTML66f232c

 

 

 

להשתמש בקונטרול הנכון

ההתחלה החשובה ביותר היא כמובן להתחיל מהמחלקה הנכונה. למרות שמה שהוא רצה *נראה* כמו ToggleButton, ההתנהגות שהוא מבקש הרבה יותר קרובה ל RadioButton. אם נתחיל משם יהיה לנו הרבה יותר קל, ולפיכך זה יהיה הצעד הראשון:

 

<UniformGrid Rows="6" Columns="2" HorizontalAlignment="Left" >
    <UniformGrid.Resources>
        <Style TargetType="ToggleButton">
            <Setter Property="Margin" Value="2" />
            <Setter Property="Padding" Value="10,5" />
        </Style>
    </UniformGrid.Resources>
    <RadioButton Content="tool1"/>
    <RadioButton Content="tool2"/>
    <RadioButton Content="tool3"/>
    <RadioButton Content="tool4"/>
    <RadioButton Content="tool5"/>
    <RadioButton Content="tool6"/>
</UniformGrid>

 

SNAGHTML59abbfe

רק שכמובן, כרגע זה לא ממש נראה כמו שהוא ביקש.
קל לתקן את זה – פשוט נגדיר Style שישכתב את ה Template של ה RadioButton:

 

<Style TargetType="RadioButton">
    <Setter Property="Margin" Value="2" />
    <Setter Property="Padding" Value="10,5" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="RadioButton">
                <Border BorderThickness="1" BorderBrush="#999" CornerRadius="2"  x:Name="theBorder">
                    <Border.Background>
                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                            <GradientStop Color="#FFEBEBEB" Offset="0.492"/>
                            <GradientStop Color="#FFDDDDDD" Offset="0.518"/>
                            <GradientStop Color="#FFCFCFCF" Offset="0.968"/>
                        </LinearGradientBrush>
                        
                    </Border.Background>
                    <Border x:Name="shadowBorder" BorderThickness="0,0,0,0" BorderBrush="#66000000" >
                        <ContentPresenter x:Name="theCP" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                    </Border>
                    
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="Background" TargetName="theBorder" >
                            <Setter.Value>
                                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                    <GradientStop Color="#FFC2E4F6" Offset="0.494"/>
                                    <GradientStop Color="#FFAADAF3" Offset="0.517"/>
                                    <GradientStop Color="#FF92CCEC" Offset="1"/>
                                </LinearGradientBrush>
 
                            </Setter.Value>
                        </Setter>
                        <Setter Property="Margin" TargetName="theCP" Value="1,1,0,0" />
                        <Setter Property="BorderThickness" TargetName="shadowBorder" Value="1,1,0,0" />
                        
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

 

וקיבלנו ToggleButtons שאפשר לבחור בהם רק אחד בכל רגע נתון.

SNAGHTML66dd6a7

בשלב הזה, נהיה מעוניינים לעבוד עם הקונטרולים הללו בצורה הסטנדרטית – ב MVVM. היינו רוצים שה ViewModel יכיל רשימה של “כלים” ואליה נקשור את רשימת הכפתורים שלנו. בנוסף נרצה שה ViewModel יחזיק גם את ה “כלי” שנבחר – כך ששינוי בUI יעדכן את ה ViewModel, ושינוי ב ViewModel  ישנה את ה UI.
המטרה היא שה ViewModel יראה כך:

public class MainWindowViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<String> tools;
        public ObservableCollection<String> Tools
        {
            get { return tools; }
            set
            {
                tools = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Tools"));
            }
        }
 
        private string selectedTool;
        public string SelectedTool
        {
            get { return selectedTool; }
            set
            {
                selectedTool = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedTool"));
            }
        }
 
 
 
        public MainWindowViewModel()
        {
            Tools = new ObservableCollection<String>();
            Tools.Add("Tool1");
            Tools.Add("Tool2");
            Tools.Add("Tool3");
            Tools.Add("Tool4");
            Tools.Add("Tool5");
            Tools.Add("Tool6");
            Tools.Add("Tool7");
 
            SelectedTool = "Tool2";
        }
 
 
 
        public event PropertyChangedEventHandler PropertyChanged;
    }

 

אבל אין לנו איך לקשור את ה UI בצורה נכונה. אז איך ממשים את זה ב MVVM?

 

 

 

מימוש נכון ב MVVM

גם כאן, כמו בהרבה מקרים אחרים ב MVVM – במידה ורוצים להתאים את ה UI ל Binding של ה ViewModel אבל אין לנו איך לקשור את זה בצורה נכונה – הפיצ’ר החזק Behaviors יעזור לנו. נגדיר Behavior חדש למטרה זו בדיוק – Behavior שאפשר לשים רק על RadioButton:

 

public class RadioButtonSelectorBehavior : Behavior<RadioButton>
{ 
 
}

 

המטרה היא שה Behavior הזה יחשוף שני מאפיינים – Value – שייצג את הערך של ה - RadioButton,  ו SelectedValue – שיקשר לערך שכרגע נבחר. במידה וה SelectedValue ישתנה – הBehavior  ידאג לעדכן את ה RadioButton – וההפך – במידה וה RadioButton נלחץ – ה Behavior יעדכן את ה SelectedValue:

אלו יהיו המאפיינים שנוסיף:

public class RadioButtonSelectorBehavior : Behavior<RadioButton>
{
    public object Value
    {
        get { return (object)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), typeof(RadioButtonSelectorBehavior), new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = true });
 
 
    public object SelectedValue
    {
        get { return (object)GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for SelectedValue.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register("SelectedValue", typeof(object), typeof(RadioButtonSelectorBehavior), new FrameworkPropertyMetadata(null, SelectedValueChanged) { BindsTwoWayByDefault = true });
 
}

והרישום הרלוונטי לאירועים:

 

 
        static void SelectedValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            RadioButtonSelectorBehavior b = sender as RadioButtonSelectorBehavior;
 
            if (b.AssociatedObject != null)
                b.CheckSelection();
        }
 
        protected override void OnAttached()
        {
 
            this.AssociatedObject.Checked += new RoutedEventHandler(AssociatedObject_Checked);
            this.AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
            
            
        }
 
        void CheckSelection()
        {
            this.AssociatedObject.IsChecked =
                this.Value.Equals(this.SelectedValue);
        }
 
 
        void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            
            CheckSelection();
 
        }
 
        void AssociatedObject_Checked(object sender, RoutedEventArgs e)
        {
            SelectedValue = Value;
        }

 

הקוד המלא של ה Behavior יראה כך:

 

public class RadioButtonSelectorBehavior : Behavior<RadioButton>
{
    public object Value
    {
        get { return (object)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for Value.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), typeof(RadioButtonSelectorBehavior), new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = true });
 
 
    public object SelectedValue
    {
        get { return (object)GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for SelectedValue.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register("SelectedValue", typeof(object), typeof(RadioButtonSelectorBehavior), new FrameworkPropertyMetadata(null, SelectedValueChanged) { BindsTwoWayByDefault = true });
 
    static void SelectedValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        RadioButtonSelectorBehavior b = sender as RadioButtonSelectorBehavior;
 
        if (b.AssociatedObject != null)
            b.CheckSelection();
    }
 
    protected override void OnAttached()
    {
 
        this.AssociatedObject.Checked += new RoutedEventHandler(AssociatedObject_Checked);
        this.AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
        
        
    }
 
    void CheckSelection()
    {
        this.AssociatedObject.IsChecked =
            this.Value.Equals(this.SelectedValue);
    }
 
 
    void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        
        CheckSelection();
 
    }
 
    void AssociatedObject_Checked(object sender, RoutedEventArgs e)
    {
        SelectedValue = Value;
    }
 
}

 

בשלב הזה הכל מוכן, וכל מה שנותר לנו הוא להשתמש במה שבנינו ב XAML שלנו:

 

 

<ItemsControl ItemsSource="{Binding Tools}" HorizontalAlignment="Left" >
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <RadioButton GroupName="tools" Content="{Binding }" >
                <i:Interaction.Behaviors>
                    <my:RadioButtonSelectorBehavior Value="{Binding .}" SelectedValue="{Binding Source={StaticResource vm}, Path=SelectedTool}" />
                </i:Interaction.Behaviors>
            </RadioButton>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Rows="8" Columns="2" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

 

והקוד המלא של החלון יראה כך:

 

<Window x:Class="ToggleRadioButtons.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:ToggleRadioButtons"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <my:MainWindowViewModel x:Key="vm" />
        <Style TargetType="RadioButton">
            <Setter Property="Margin" Value="2" />
            <Setter Property="Padding" Value="10,5" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="RadioButton">
                        <Border BorderThickness="1" BorderBrush="#999" CornerRadius="2"  x:Name="theBorder">
                            <Border.Background>
                                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                    <GradientStop Color="#FFEBEBEB" Offset="0.492"/>
                                    <GradientStop Color="#FFDDDDDD" Offset="0.518"/>
                                    <GradientStop Color="#FFCFCFCF" Offset="0.968"/>
                                </LinearGradientBrush>
 
                            </Border.Background>
                            <Border x:Name="shadowBorder" BorderThickness="0,0,0,0" BorderBrush="#66000000" >
                                <ContentPresenter x:Name="theCP" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                            </Border>
 
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter Property="Background" TargetName="theBorder" >
                                    <Setter.Value>
                                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                            <GradientStop Color="#FFC2E4F6" Offset="0.494"/>
                                            <GradientStop Color="#FFAADAF3" Offset="0.517"/>
                                            <GradientStop Color="#FF92CCEC" Offset="1"/>
                                        </LinearGradientBrush>
 
                                    </Setter.Value>
                                </Setter>
                                <Setter Property="Margin" TargetName="theCP" Value="1,1,0,0" />
                                <Setter Property="BorderThickness" TargetName="shadowBorder" Value="1,1,0,0" />
 
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Window.DataContext>
        <StaticResourceExtension ResourceKey="vm" />
    </Window.DataContext>
 
        <Grid>
    
        <ItemsControl ItemsSource="{Binding Tools}" HorizontalAlignment="Left" >
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <RadioButton GroupName="tools" Content="{Binding }" >
                        <i:Interaction.Behaviors>
                            <my:RadioButtonSelectorBehavior Value="{Binding .}" SelectedValue="{Binding Source={StaticResource vm}, Path=SelectedTool}" />
                        </i:Interaction.Behaviors>
                    </RadioButton>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Rows="8" Columns="2" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <!--<UniformGrid Rows="6" Columns="2" HorizontalAlignment="Left" >
            <UniformGrid.Resources>
            </UniformGrid.Resources>
            <RadioButton Content="tool1"/>
            <RadioButton Content="tool2"/>
            <RadioButton Content="tool3"/>
            <RadioButton Content="tool4" IsChecked="True"/>
            <RadioButton Content="tool5"/>
            <RadioButton Content="tool6"/>
        </UniformGrid>-->
    </Grid>
</Window>

 

 

בשורה התחתונה, שימוש בBehavior יכול מאוד להקל את העבודה של ה ViewModel ולהפוך אותו להרבה יותר ברור ופשוט.
יש לציין שעקרונית אפשר היה להתחיל מ ListBox ולא מרשימת RadioButtons – אבל זו דרך קשה יותר מבחינה טכנית מאשר הדרך בה בחרנו, למרות שהיא בפירוש נכונה. הדרך המוצעת לעיל היא הדרך הכי פשוטה לדעתי.

אפשר להוריד את הקוד כאן