WPF - Should a user control have its own ViewModel?

后端 未结 5 1008
梦毁少年i
梦毁少年i 2020-11-27 14:32

I have a window made up of several user controls and was wondering whether each user control have its own ViewModel or should the window as a whole have only one ViewModel?<

5条回答
  •  情书的邮戳
    2020-11-27 15:00

    Absolutely, positively

    NO

    Your UserControls should NOT have ViewModels designed specifically for them. This is, in fact, a code smell. It doesn't break your application immediately, but it will cause you pain as you work with it.

    A UserControl is simply an easy way to create a Control using composition. UserControls are still Controls, and therefore should solely be concerned with matters of UI.

    When you create a ViewModel for your UserControl, you are either placing business or UI logic there. It is incorrect to use ViewModels to contain UI logic, so if that is your goal, ditch your VM and place the code in that control's codebehind. If you're placing business logic in the UserControl, most likely you are using it to segregate parts of your application rather than to simplify control creation. Controls should be simple and have a single purpose for which they are designed.

    When you create a ViewModel for your UserControl, you also break the natural flow of data via the DataContext. This is where you will experience the most pain. To demonstrate, consider this simple example.

    We have a ViewModel that contains People, each being an instance of the Person type.

    public class ViewModel
    {
        public IEnumerable People { get; private set; }
        public ViewModel()
        {
            People = PeopleService.StaticDependenciesSuckToo.GetPeople();
        }
    }
    
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
    

    To show a list of people in our window is trivial.

    
        
            
        
        
            
                
            
        
        
    
    

    The list automatically picks up the correct item template for the Person and uses the PersonView to display the person's information to the user.

    What is PersonView? It is a UserControl that is designed to display the person's information. It's a display control for a person, similarly to how the TextBlock is a display control for text. It is designed to bind against a Person, and as such works smoothly. Note in the window above how the ListView transfers each Person instance to a PersonView where it becomes the DataContext for this subtree of the visual.

    
        
            
            
            
            
        
    
    

    For this to work smoothly, the ViewModel of the UserControl must be an instance of the Type it is designed for. When you break this by doing stupid stuff like

    public PersonView()
    {
        InitializeComponent();
        this.DataContext = this; // omfg
    }
    

    or

    public PersonView()
    {
        InitializeComponent();
        this.DataContext = new PersonViewViewModel();
    }
    

    you've broken the simplicity of the model. Usually in these instances you end up with abhorrent workarounds, the most common of which is creating a pseudo-DataContext property for what your DataContext should actually be. And now you can't bind one to the other, so you end up with awful hacks like

    public partial class PersonView : UserControl
    {        
        public PersonView()
        {
            InitializeComponent();
            var vm = PersonViewViewModel();
            // JUST KILL ME NOW, GET IT OVER WITH 
            vm.PropertyChanged = (o, e) =>
            {
                if(e.Name == "Age" && MyRealDataContext != null)
                    MyRealDataContext.Age = vm.PersonAge;
            };
            this.DataContext = vm; 
        }
        public static readonly DependencyProperty MyRealDataContextProperty =
            DependencyProperty.Register(
                "MyRealDataContext",
                typeof(Person),
                typeof(PersonView),
                new UIPropertyMetadata());
        public Person MyRealDataContext
        {
            get { return (Person)GetValue(MyRealDataContextProperty); }
            set { SetValue(MyRealDataContextProperty, value); }
        }
    }
    

    You should think of a UserControl as nothing more than a more complex control. Does the TextBox have its own ViewModel? No. You bind your VM's property to the Text property of the control, and the control shows your text in its UI.

    MVVM doesn't stand for "No Codebehind". Put your UI logic for your user control in the codebehind. If it is so complex that you need business logic inside the user control, that suggests it is too encompassing. Simplify!

    Think of UserControls in MVVM like this--For each model, you have a UserControl, and it is designed to present the data in that model to the user. You can use it anywhere you want to show the user that model. Does it need a button? Expose an ICommand property on your UserControl and let your business logic bind to it. Does your business logic need to know something going on inside? Add a routed event.

    Normally, in WPF, if you find yourself asking why it hurts to do something, it's because you shouldn't do it.

提交回复
热议问题