Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind to Model or ViewModel

Tags:

c#

mvvm

wpf

I created a project using MVVM pattern (or so I thought ;) ). To simplify my case:

Model:

public class Model {
    public string Name { get; set; }
    public bool IsDefective { get; set; }
}

ViewModel - extends MvvmLight ViewModelBase:

public class ViewModel : ViewModelBase {
    private ObservableCollection<Model> models;
    public ObservableCollection<Model> Models {
        get {
            if (_models== null) {
                _models= new ObservableCollection<Models>();
            }

            return _models;
        }
        set {
            RaisePropertyChanged("Models");

            _models= value;
        }
    }
}

View - I'm showing a list of textboxes:

<TextBlock Text="{Binding Name}">
    <TextBlock.Style>
        <Style TargetType="{x:Type TextBlock}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=.IsDefective}" Value="True">
                    <Setter Property="Foreground" Value="Red" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </TextBlock.Style>
</TextBlock>

My scenario is like so: some methods in the Model class may change the IsDefective property, but since my model does not implement the INotifyPropertyChanged interface, my view does not know about such changes. How should this problem be resolved "the mvvm way"? I stumbled upon this question here in SO, but to be honest after reading both highest voted answers and the discussion in comments, I'm more confused: MVVM - PropertyChanged in Model or ViewModel? . I'm willing to agree with Jonathan Allen, because it's just more natural for me to bind this way, but as a beginner in the mvvm pattern I may be missing something. So, am I?

like image 401
Marek M. Avatar asked Sep 07 '14 20:09

Marek M.


1 Answers

Generally you want your model to be a dumb data transfer object. When you do a database query, you get a dumb model back that doesn't do any transformations because otherwise you're failing to follow Separation of Concerns in SOLID principals. However, cheating a little won't kill you, but it might make debugging something a little frustrating because most people won't expect their POCO (plain old CLR object) to initiate any business logic.

Here's some code:

Some setup classes:

ViewModelBase.cs

A "smarter" version of the ViewModelBase from galasoft, this bad boy autowires up design time view models (you'll like this one)

namespace WPFPlayground.ViewModel
{
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void SetValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
        {
            if (property != null)
            {
                if (property.Equals(value)) return;
            }

            OnPropertyChanged(propertyName);
            property = value;
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

DefectiveToBackgroundColorConverter.cs

A value converter for our use when our product is being displayed on the view (you'll see it referenced later):

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

namespace WPFPlayground
{
    public class DefectiveToBackgroundColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (System.Convert.ToBoolean(value))
            {
                return new SolidColorBrush(Colors.Red);
            }
            return new SolidColorBrush(Colors.White);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }
    }
}

Using Model-first method:

ProductModel.cs

POCO DTO

namespace WPFPlayground.Model
{
    public class ProductModel
    {
        public string Name { get; set; }
        public bool IsDefective { get; set; }
    }
}

ProductViewModel.cs

Notice the use of setvalue to automatically wire up the notifypropertychanged event.

namespace WPFPlayground.ViewModel
{
    public class ProductViewModel : ViewModelBase
    {
        private string _name;
        private bool _isDefective;

        public bool IsDefective
        {
            get { return _isDefective; }
            set { SetValue(ref _isDefective, value); }
        }

        public string Name
        {
            get { return _name; }
            set { SetValue(ref _name, value); }
        }
    }
}

So we have a productmodel and a productviewmodel. One does all the work when you're interacting with the database, and one does all the work when you bind to your views.

So we'll need a view that represents just a single productviewmodel:

ProductView.xaml

Notice the use of the background color converter to handle our triggers

<UserControl x:Class="WPFPlayground.View.ProductView"
             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:wpfPlayground="clr-namespace:WPFPlayground"
             mc:Ignorable="d" 
             d:DataContext="{d:DesignInstance wpfPlayground:DesignProductViewModel, IsDesignTimeCreatable=True}">
    <UserControl.Resources>
        <wpfPlayground:DefectiveToBackgroundColorConverter x:Key="DefectiveToBackgroundColorConverter" />
    </UserControl.Resources>
    <Viewbox>
        <Border Width="500" Background="{Binding IsDefective, Converter={StaticResource DefectiveToBackgroundColorConverter}}">
            <TextBlock Text="{Binding Name}" FontSize="40" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center" />
        </Border>
    </Viewbox>
</UserControl>

Next we'll need that design time viewmodel so we can view our XAML in design time:

DesignProductViewModel.cs

A bit boring, but it makes design time work!

using WPFPlayground.ViewModel;

namespace WPFPlayground
{
    public class DesignProductViewModel : ProductViewModel
    {
        public DesignProductViewModel()
        {
            Name = "This is my product";
            IsDefective = true;
        }
    }
}

Now we need to display a list of these viewmodels:

MainWindow.xaml

Itemscontrol all day err day

<Window x:Class="WPFPlayground.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModel="clr-namespace:WPFPlayground.ViewModel"
        xmlns:view="clr-namespace:WPFPlayground.View"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" d:DataContext="{d:DesignInstance viewModel:DesignProductsViewModel, IsDesignTimeCreatable=True}">
    <Window.Resources>
        <DataTemplate DataType="{x:Type viewModel:ProductViewModel}">
            <view:ProductView />
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <ItemsControl ItemsSource="{Binding Products}">
            <view:ProductView />
        </ItemsControl>
    </StackPanel>
</Window>

DesignProductsViewModel.cs

The design time view model so you can see this working in design time. It generates an easy random set of products.

using System;
using System.Collections.ObjectModel;
using System.Linq;

namespace WPFPlayground.ViewModel
{
    public class DesignProductsViewModel : ProductsViewModel
    {
        public DesignProductsViewModel()
        {
            var random = new Random();
            Products = new ObservableCollection<ProductViewModel>(Enumerable.Range(1, 5).Select(i => new ProductViewModel
            {
                Name = String.Format(@"Product {0}", i),
                IsDefective = (random.Next(1, 100) < 50)
            }));
        }
    }
}
like image 182
C Bauer Avatar answered Oct 09 '22 00:10

C Bauer