10

I just started learning MVVM and here is what seems to be basic question but I spent whole day trying to figure it out.

I have a solution that contains 3 projects one for Model, one for ViewModel and one for View. The Model contains a class that has 2 properties Text and CheckStatus.

The ViewModel has a list called listOfItems that has three items, each item has these 2 properties from the Model.

The View has a listView inside it there is a CheckBox. What is the proper way to bind the CheckBox content to the property Text?

Here is the model

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace TheModel
{
public class CheckBoxListModel : INotifyPropertyChanged
{
    private string text;
    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaiseChanged("Text");
        }
    }

    private bool checkStatus;
    public bool CheckStatus
    {
        get { return checkStatus; }
        set
        {
            checkStatus = value;
            RaiseChanged("CheckStatus");
        }
    }

    private void RaiseChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
   }
}

Here is the view model

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using TheModel;

namespace TheViewModel
{
public class TheViewModel
{
    public List<CheckBoxListModel> ListOfItems { get; set; }

    public TheViewModelClass()
    {
        ListOfItems = new List<CheckBoxListModel>
        {
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 1",
        },
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 2",
        },
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 3",
        }
    };
    }

    public static implicit operator List<object>(TheViewModelClass v)
    {
        throw new NotImplementedException();
    }
   }
}

and here is the View XAML

 <UserControl
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:ctrl="clr-namespace:TheView.Managers" xmlns:TheViewModel="clr-
 namespace:TheViewModel;assembly=TheViewModel" 
 x:Class="TheView.Styles.ListViewDatabaseStyle">

<UserControl.DataContext>
    <TheViewModel:TheViewModelClass/>
</UserControl.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="100"/>
    </Grid.RowDefinitions>
    <Button Content="Continue" Style="{StaticResource ButtonStyle}" 
          Margin="1104,27,40,40"/>
    <ListView x:Name="listView1" SelectionMode="Multiple" 
              Style="{StaticResource ListViewStyle}" Margin="10,55,10,10"
              ctrl:ListViewLayoutManager.Enabled="true" ItemsSource="
          {Binding TheViewModelClass}" >
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Competency Items" 
                  ctrl:ProportionalColumn.Width="1100"/>
            </GridView>
        </ListView.View>
        <ListView.ItemContainerStyle >
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="IsSelected" Value="{Binding 
                             CheckedStatus}"/>
                <Setter Property="HorizontalContentAlignment" 
                              Value="Stretch"/>
            </Style>
        </ListView.ItemContainerStyle>
        <ListView.ItemTemplate>
            <DataTemplate>
                <CheckBox  
                     Click="CheckBox_Click"
                     Content="{Binding Path=TheViewModelClass.Text}"
                     IsChecked="{Binding 
                     Path=TheViewModelClass.CheckedStatus}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>
</UserControl>

Here is the View behind code, I know I shouldn't have something here but where should that part go?

using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System;
using System.Text;
using TheViewModel;

namespace TheView.Styles
{
public partial class ListViewDatabaseStyle : UserControl
{
    public ListViewDatabaseStyle()
    {
        InitializeComponent();
    }

    public List<string> selectedNames = new List<string>();
    private void CheckBox_Click(object sender, RoutedEventArgs e)
    {
        var ChkBox = sender as CheckBox;
        var item = ChkBox.Content;
        bool isChecked = ChkBox.IsChecked.HasValue ? ChkBox.IsChecked.Value 
         : false;
        if (isChecked)
            selectedNames.Add(item.ToString());
        else
            selectedNames.Remove(item.ToString());
    }
  }
 }
  • The proper way is to make sure the DataContext is set correctly to your Model or ViewModel, and to use a Binding to pull the value from the data object into the UI. Have you encountered the DataContext yet in your learning? If not, I usually send new coders to this post : [What is DataContext for?](http://stackoverflow.com/a/7262322/302677) – Rachel Apr 26 '17 at 18:43
  • I haven't figured the correct way to use the DataContext so that it works. I have tried several times and I saw it implemented in several way but none worked for me –  Apr 26 '17 at 19:22
  • deleted part of the behind code for the datacontext because that was the wrong par. and sorry @Peter your solution didn't work –  Apr 26 '17 at 19:30
  • It's recommended to have views and viewmodels in the same project, since they both belongs to presentation layer. They logically belongs together – Liero Apr 26 '17 at 19:34
  • 1
    If i were you i would change list for ObservableCollection. public ObservableCollection ListOfItems { get; set; } And trust me. You don't have to add every binding your viewmodel. In your Listview ItemSource={Binding ListOfItems} – Péter Hidvégi Apr 26 '17 at 19:45
  • I'm trying now to make the ObservableCollection. but what you mean I don't have to add every binding to my ListView and CheckBox and IsChecked? how else they will "Know" what they are binding to ? –  Apr 26 '17 at 19:53
  • 1
    By the way - you may find help in the [WPF chat room](http://chat.stackoverflow.com/rooms/18165/wpf). We're there most weekdays during work hours. – Lynn Crumbling Apr 26 '17 at 20:05
  • thanks, I have been trying and I cannot understand and it's not working and I know I would understand if someone just post a code that would actually work in this case. I know yours works but if you try it you will see it won't and then you might figure what I did wrong somewhere else maybe in the code. and I can't join chat now needs 20 points :) thanks for all –  Apr 26 '17 at 20:13
  • You have 24 points. Give the system time to pick up on the change, but you should be able to get in shortly. – Lynn Crumbling Apr 26 '17 at 20:16

3 Answers3

8

This is all quite ridiculous.

Here is a much easier way which involves no external libraries, no additional housekeeping classes and interfaces, almost no magic, and is very flexible because you can have viewmodels that contain other viewmodels, and you get to instantiate each one of them, so you can pass constructor parameters to them:

For the viewmodel of the main window:

using Wpf = System.Windows;

public partial class TestApp : Wpf.Application
{
    protected override void OnStartup( Wpf.StartupEventArgs e )
    {
        base.OnStartup( e );
        MainWindow = new MainView();
        MainWindow.DataContext = new MainViewModel( e.Args );
        MainWindow.Show();
    }
}

For all other viewmodels:

This is in MainViewModel.cs:

using Collections = System.Collections.Generic;

public class MainViewModel
{
    public SomeViewModel SomeViewModel { get; }
    public OtherViewModel OtherViewModel { get; }
    public Collections.IReadOnlyList<string> Arguments { get; }
    
    public MainViewModel( Collections.IReadOnlyList<string> arguments )
    {
        Arguments = arguments;
        SomeViewModel = new SomeViewModel( this );
        OtherViewModel = new OtherViewModel( this );
    }
}

This in MainView.xaml:

[...]
xmlns:local="clr-namespace:the-namespace-of-my-wpf-stuff"
[...]
    <local:SomeView DataContext="{Binding SomeViewModel}" />
    <local:OtherView DataContext="{Binding OtherViewModel}" />
[...]

As you can see, a viewmodel can simply be a member (child) of another viewmodel; in this case SomeViewModel and OtherViewModel are children of MainViewModel. Then, in the XAML file of MainView, you can just instantiate each of the child views and specify their DataContext by Binding to the corresponding child viewmodels.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • 3
    This is the most underrated post in all of stack overflow. I do this all the time in Xamarin but was unable to piece it together in WPF as the syntax is a little different. Thank you so much – SQLUser Aug 01 '21 at 17:50
  • 1
    @SQLUser thanks! C-:= glad to be of help. I am amazed by the cumbersome ways in which most WPF applications are written. – Mike Nakis Aug 01 '21 at 21:58
  • 1
    lol thanks for that, I'm also reading through the other info and I was quite impressed by the amount of over-complicated stuff just to set goddamn DataContext to my object. – user0103 Jan 27 '23 at 09:50
5

First of all. Set dependencies of projects. ViewModel must have access Model. (View and Model projects do not have to reference to other projects.) If i were you i would make a StartUp Project to transfer the control to ViewModel. This "StartUp" project should be WPF, all of others should be "class library" but don't forget to add the required references to projects (For example the system.xaml for your view project to create usercontrols.)

Projects dependencies: - StartUp --> ViewModel; (- ViewModel --> View; or avoid this with DI) - ViewModel --> Model; (I should make another project for interfaces just this is just my perversions.)

StartUp Project: Now in your startup (WPF) project should contains in (app.xaml.cs):

protected override void OnStartup(StartupEventArgs e)
{
    // delete the startupuri tag from your app.xaml
    base.OnStartup(e);
    //this MainViewModel from your ViewModel project
    MainWindow = new MainWindow(new MainViewModel());
} 

The only one thing (Window) in your startup wpf project (to display your UserControls).

MainWindow.xaml content:

<Window x:Class="StartUp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" WindowState="Maximized" WindowStyle="None" AllowsTransparency="True">
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding Control}"/>
</Window>

(and xaml.cs)

  public partial class MainWindow : Window
    {
        public MainWindow(INotifyPropertyChanged ViewModel)
        {
            InitializeComponent();
            this.DataContext = ViewModel;
            this.Show();
        }
    }

And Thats all your StartUp WPF project. In this way we gave the control to your ViewModel project.

(Okay, its just an extra, but i should make a "ViewService" to handle my UserControls)

Interface to find all of View and match the View with ViewModel.

public interface IControlView
{
    INotifyPropertyChanged ViewModel { get; set; }
}

I created a singleton to store and match my views with my viewmodels. (You can skip this part.) I defined this in Model project.

 public class ViewService<T> where T : IControlView
    {
        private readonly List<WeakReference> cache;

        public delegate void ShowDelegate(T ResultView);
        public event ShowDelegate Show;
        public void ShowControl<Z>(INotifyPropertyChanged ViewModel)
        {
            if (Show != null)
                Show(GetView<Z>(ViewModel));
        }

        #region Singleton

        private static ViewService<T> instance;
        public static ViewService<T> GetContainer
        {
            get
            {
                if (instance == null)
                {
                    instance = new ViewService<T>();
                }
                return instance;
            }
        }

        private ViewService()
        {
            cache = new List<WeakReference>();
            var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()).Where(r => typeof(T).IsAssignableFrom(r) && !r.IsInterface && !r.IsAbstract && !r.IsEnum);

            foreach (Type type in types)
            {
                cache.Add(new WeakReference((T)Activator.CreateInstance(type)));
            }
        }

        #endregion

        private T GetView<Z>(INotifyPropertyChanged ViewModel)
        {
            T target = default(T);
            foreach (var wRef in cache)
            {
                if (wRef.IsAlive && wRef.Target.GetType().IsEquivalentTo(typeof(Z)))
                {
                    target = (T)wRef.Target;
                    break;
                }
            }

            if(target==null)
                target = (T)Activator.CreateInstance(typeof(Z));

            if(ViewModel != null)
                target.ViewModel = ViewModel;

            return target;
        }

    }

And now you have got a "service" to show your UserControls in the mainwindow from your ViewModel:

public class MainViewModel : INotifyPropertyChanged
    {

        private IControlView _control;
        public IControlView Control
        {
            get
            {
                return _control;
            }
            set
            {
                _control = value;
                OnPropertyChanged();
            }
        }

        public MainViewModel()
        {   //Subscribe for the ViewService event:   
            ViewService<IControlView>.GetContainer.Show += ShowControl;
            // in this way, here is how to set a user control to the window.
            ViewService<IControlView>.GetContainer.ShowControl<ListViewDatabaseStyle>(new TheViewModel(yourDependencyItems));
           //you can call this anywhere in your viewmodel project. For example inside a command too.
        }

        public void ShowControl(IControlView ControlView)
        {
            Control = ControlView;
        }

        //implement INotifyPropertyChanged...
        protected void OnPropertyChanged([CallerMemberName] string name = "propertyName")
        {
           PropertyChangedEventHandler handler = PropertyChanged;
           if (handler != null)
           {
               handler(this, new PropertyChangedEventArgs(name));
           }
        }

           public event PropertyChangedEventHandler PropertyChanged;
    }

If you don't want to use this "ViewService". Just create an UserControl instance, match DataContext of View with your ViewModel and give this view to Control property. Here is your ViewModel with list (still in ViewMoldel project.)

public class TheViewModel
    {
        private readonly ObservableCollection<ISelectable> listOfItems;
        public ObservableCollection<ISelectable> ListOfItems 
        {
            get { return listOfItems; }
        }

        public ICommand SaveCheckedItemsText{
            get{ return new RelayCommand(CollectNamesOfSelectedElements);}
        }

        public IEnumerable<ISelectable> GetSelectedElements
        {
            get { return listOfItems.Where(item=>item.CheckStatus); }
        }

        public TheViewModel(IList<ISelectable> dependencyItems)
        {
            listOfItems= new ObservableCollection<ISelectable>(dependencyItems);
        }

        //here is your list...
        private List<string> selectedNames

        //use this...
        private void CollectNamesOfSelectedElements()
        {
           selectedNames = new List<string>();
           foreach(ISelectable item in GetSelectedElements)
           {
             //you should override the ToString in your model if you want to do this...
             selectedNames.Add(item.ToString());
           }
        }

    }

RelayCommand article

View: (Keep here all of your usercontrols.)

In your UserControl (xaml):

<UserControl x:Class="View.ListViewDataStyle"
             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" namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             mc:Ignorable="d">
<Button Command={Binding SaveCheckedItemsText}/>
<!-- Another content -->
    <ListView ItemsSource="{Binding ListOfItems}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Text}" IsChecked="{Binding CheckedStatus}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</UserControl>

And with interface here is the xaml.cs code (for UserControls):

public partial class ListViewDatabaseStyle : UserControl, IControlView
    {
        public ListViewDatabaseStyle ()
        {
            InitializeComponent();
        }

        public INotifyPropertyChanged ViewModel
        {
            get
            {
                return (INotifyPropertyChanged)DataContext;
            }
            set
            {
                DataContext = value;
            }
        }
    }

And the last one is the Model project with your models:

 public interface ISelectable
    {
        bool CheckStatus { get; set; }
    }

public class CheckBoxListModel : INotifyPropertyChanged, ISelectable
{
    private string text;
    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaiseChanged("Text");
        }
    }

    private bool checkStatus;
    public bool CheckStatus
    {
        get { return checkStatus; }
        set
        {
            checkStatus = value;
            RaiseChanged("CheckStatus");
        }
    }

    private void RaiseChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
   }
}

Excuse me for english grammar mistakes, i hope you understood my post.

Update: Use the DI techn. to avoid the reference to view from viewmodel. DI service will inject the correct object with constructor injection.

Péter Hidvégi
  • 743
  • 6
  • 21
  • 1
    Many thanks Peter for your detailed explain. I will try to do the same and I'm sure it will work this time. Thanks again. :) –  Apr 27 '17 at 05:41
  • small question, the ViewService and the singleton you mentioned, where that should be created? –  Apr 27 '17 at 07:15
  • I defined this in the Model Project. Because of dependencies you can use this anywhere in your ViewModel project. – Péter Hidvégi Apr 27 '17 at 07:24
  • So the "ViewService", the "IControlView" and "ISelectable" are they part of the class library called "Model" file, or separate classes inside the that class library? if you know what I mean –  Apr 27 '17 at 07:27
  • I made another project for interfaces and all projects have access for this interface project. So you can define all of your interfaces in this separated project. In this way IControlView and ISelectable in Interfaces project and ViewService class in Model project. By the way all of project are "class library" type (except the StartUp WPF project). – Péter Hidvégi Apr 27 '17 at 07:42
  • In your example "public partial class ListViewDatabaseStyle" isn't that one should be "public partial class ListViewDataStyle" –  Apr 27 '17 at 08:00
  • Can you please check the structure here and see if it's correct. and if you please can add the references, I'm little confused about them https://gist.github.com/NewCoderNotInTown/75c50ad03a12ead85acbb4fdca3cb9f8 –  Apr 27 '17 at 08:10
  • I think its correct, but you missed your CheckBoxListModel model from Project Type: ClassLibrary, Project Name: Model Class: ViewService (Class: CheckBoxListModel) – Péter Hidvégi Apr 27 '17 at 08:16
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/142801/discussion-between-newcoderintown-and-peter-hidvegi). –  Apr 27 '17 at 08:22
  • thanks for your help Peter, but after all this and adding all the cool stuff to the software, it still doesn't do what I simply want it to do, which is having the list with checkboxes display several items and with a button the checked items are copied to another list .. it's that simple :) –  Apr 27 '17 at 09:36
  • I think that was mistake, both answers are correct and useful but they do not do what I need. sorry if I sounded rude, you guys help a lot and maybe I'm not explaining the issue clearly :) my apologise –  Apr 27 '17 at 09:54
  • No problem. I was just surprised. I thought i made some mistakes. – Péter Hidvégi Apr 27 '17 at 10:20
  • The view model absolutely does not have to have a reference to the view. It can expose interfaces for whatever it needs in relation to the view, implementations of which can then be supplied by the view itself. –  Apr 27 '17 at 14:35
  • I agree with you. The pure mvvm viewmodel shouldn`t reference for View project. – Péter Hidvégi Apr 27 '17 at 15:07
  • I had trouble following this answer; it's very convoluted. – Zimano Jan 02 '19 at 10:28
1
<UserControl.DataContext>
    <TheViewModel:TheViewModelClass/>
</UserControl.DataContext>

<ListView ItemsSource="{Binding ListOfItems}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Text}" IsChecked="{Binding CheckedStatus}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Liero
  • 25,216
  • 29
  • 151
  • 297
  • this didn't work, it's same what Peter suggested. It shows like this: TheModel.CheckBoxListModel TheModel.CheckBoxListModel TheModel.CheckBoxListModel –  Apr 26 '17 at 19:43
  • 1
    But this is how it should work. The problem is probably not in your xaml, but it the code you haven't shown us. I see that you have manipulated with the code when copy&pasting here, because there are errors in your viewmodel. Nevermind, this is the right way to do it. Check your Output windows when debugging the app. BTW, haven't you modified the ControlTemplate of CheckBox? – Liero Apr 26 '17 at 20:30
  • yes I have modified it and yes the code is different because i'm working on a copy from it. hopefully with some help I'll be able to understand why it's not working :) thanks all –  Apr 26 '17 at 20:47