2

I have a checklist view that has 2 ScrollViewers. One checklist is for incomplete items, the other is for complete items. They are populated by 2 separate observable collections and bound to by ItemsControls.

The UserControl has a button, when clicked will move that 'check' to the other collection.

Currently the way I have this setup is in the ViewModel that's the DataContext for the UserControl there is a public event that is subscribed to by the main window's VM by using:

((CheckItemVM) ((CheckListItem) cli).DataContext).CompleteChanged += OnCompleteChanged;

where cli is the checklist item.

then the OnCompleteChanged finds the appropriate View object by using:

foreach (object aCheck in Checks)
        {
            if (aCheck.GetType() != typeof (CheckListItem)) continue;
            if (((CheckListItem) aCheck).DataContext == (CheckItemVM) sender)
            {
                cliToMove = (CheckListItem) aCheck;
                break;
            }
        }

It's pretty obvious this breaks MVVM and I'm looking for a way around it (CheckListItem is the View, and CheckItemVM is it's DataContext ViewModel). Reasoning for the boxed type is I've got another UserControl that will have instances inside both, which are basically section labels, and I need to be able to sort my observable collections where there is an association between the checklistitem to a specific section by name.

phillk6751
  • 188
  • 15
  • 2
    I'm confused, if you have two lists each bound to a separate `ObservableCollection`, then why don't you bind your button to an `ICommand` on the ViewModel that removes any items from collection1 where IsSelected=true and places them in collection2? Alternatively, pass the selected items in the CommandParameter to the `ICommand` if you're not binding the checked value to the object model somehow. – Rachel Mar 25 '15 at 19:39
  • The Main Window's VM is what holds the ObservableCollection which is a collection of CheckListItem UserControls. The binding of the button is bound to a property in the CheckListItem VM. The CompleteChanged event is called by the property setter to report back to the CheckListItem VM so it knows that the complete status changed. – phillk6751 Mar 25 '15 at 20:12
  • If you're holding a collection of UserControls in your VM then you've already broken MVVM. – goobering Mar 25 '15 at 20:25
  • Okay, so how would I fix it to follow MVVM? – phillk6751 Mar 25 '15 at 20:27
  • Rework your UserControls to expose Dependency Properties rather than binding them to their own VMs. Then use a single VM for the whole View. Per Rachel's suggestion, use 2 ObservableCollections to hold your CheckListItems and MovedCheckListItems. Use these as the DataContext for your new UserControls. Bind your 'Move' button to a command on the VM which finds the checked items, adds them to the other list and removes them from the original list. – goobering Mar 25 '15 at 20:36
  • 1
    You need to completely separate your data from your UI. On the Data side (DataContext), you'd have two ObservableCollections of a custom class, and an ICommand to move items from one to the other. From the XAML, you would bind two ItemsControls to your two collections, and tell them to draw each item using a CheckBox. You would also bind the `CheckBox.Checked` to a property on the data item such as `IsSelected`. The ICommand would move items where `IsSelected=true` from one list to the other, and reset the flag. – Rachel Mar 25 '15 at 20:38
  • 2
    If you're struggling to understand MVVM and the DataContext, you may also want to check out [this answer](http://stackoverflow.com/questions/15681352/transitioning-from-windows-forms-to-wpf/15684569#15684569) of mine. I like to blog about WPF beginner topics, so the linked articles may also help you out. – Rachel Mar 25 '15 at 20:39
  • goobering: so then when i instantiate the View for the UserControls, the constructor in the code behind would be called like "new CheckListItem(this)" then the constructor would do "this.DataContext = parentVM" where parentVM is the reference to the Main Window's VM? – phillk6751 Mar 25 '15 at 20:40
  • For Clarity...the UserControl has it's own collection of data (a comments cell, and 3 other cells that hold either check boxes like yes/no or pass/fail, or radio buttons) that is filled out by the user – phillk6751 Mar 25 '15 at 20:44
  • 1
    I think at this point I'd strongly recommend reading some of Rachel's suggestions - her blog is *wonderfully* useful. I'm not 100% clear on the structure of your program, but it looks like you may have gone a little astray in your understanding of the fundamentals of MVVM. – goobering Mar 25 '15 at 20:57

1 Answers1

4

This can be done in MVVM using commands, and bindings....

The idea that I propouse here is to create a command in the Windows view model, that manage the check command, and this command to receive the item view model in the params, then manage the the things in the command. I'm going to show you a simple example, using MvvmLight library:

enter image description here

The model:

public class ItemViewModel : ViewModelBase
{
    #region Name

    public const string NamePropertyName = "Name";

    private string _name = null;

    public string Name
    {
        get
        {
            return _name;
        }

        set
        {
            if (_name == value)
            {
                return;
            }

            RaisePropertyChanging(NamePropertyName);
            _name = value;
            RaisePropertyChanged(NamePropertyName);
        }
    }

    #endregion

    #region IsChecked

    public const string IsCheckedPropertyName = "IsChecked";

    private bool _myIsChecked = false;
    public bool IsChecked
    {
        get
        {
            return _myIsChecked;
        }
        set
        {
            if (_myIsChecked == value)
            {
                return;
            }

            RaisePropertyChanging(IsCheckedPropertyName);
            _myIsChecked = value;
            RaisePropertyChanged(IsCheckedPropertyName);
        }
    }

    #endregion

}

A simple model with two property, one for the name (an identifier) and another for the check status.

Now in the Main View Model, (or Windows view model like you want)....

First the Collections, one for the checked items, and another for the unchecked items:

    #region UncheckedItems

    private ObservableCollection<ItemViewModel> _UncheckedItems;

    public ObservableCollection<ItemViewModel> UncheckedItems
    {
        get { return _UncheckedItems ?? (_UncheckedItems = GetAllUncheckedItems()); }
    }

    private ObservableCollection<ItemViewModel> GetAllUncheckedItems()
    {
        var toRet = new ObservableCollection<ItemViewModel>();

        foreach (var i in Enumerable.Range(1,10))
        {
            toRet.Add(new ItemViewModel {Name = string.Format("Name-{0}", i), IsChecked = false});
        }

        return toRet;
    }        

    #endregion

    #region CheckedItems

    private ObservableCollection<ItemViewModel> _CheckedItems;

    public ObservableCollection<ItemViewModel> CheckedItems
    {
        get { return _CheckedItems ?? (_CheckedItems = GetAllCheckedItems()); }
    }

    private ObservableCollection<ItemViewModel> GetAllCheckedItems()
    {
        var toRet = new ObservableCollection<ItemViewModel>();

        foreach (var i in Enumerable.Range(11, 20))
        {
            toRet.Add(new ItemViewModel { Name = string.Format("Name-{0}", i), IsChecked = true });
        }

        return toRet;
    }

    #endregion

And the command:

    #region CheckItem

    private RelayCommand<ItemViewModel> _CheckItemCommand;

    public RelayCommand<ItemViewModel> CheckItemCommand
    {
        get { return _CheckItemCommand ?? (_CheckItemCommand = new RelayCommand<ItemViewModel>(ExecuteCheckItemCommand, CanExecuteCheckItemCommand)); }
    }

    private void ExecuteCheckItemCommand(ItemViewModel item)
    {
        //ComandCode
        item.IsChecked = true;
        UncheckedItems.Remove(item);
        CheckedItems.Add(item);
    }

    private bool CanExecuteCheckItemCommand(ItemViewModel item)
    {
        return true;
    }

    #endregion

The magic here could be in the Data binding, in this case I used command parameter and the FindAncestor binding, check the Data Template:

        <DataTemplate x:Key="UncheckedItemDataTemplate">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
                    <CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
                    <Button Content="Check" Width="75" Command="{Binding DataContext.CheckItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Mode=OneWay}"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="CheckedItemDataTemplate">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
                    <CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
                </StackPanel>
            </Grid>
        </DataTemplate>

One data template for checked items, and another for unchecked items. Now the usage, this is simpler:

    <ListBox Grid.Row="2" Margin="5" ItemsSource="{Binding UncheckedItems}" ItemTemplate="{DynamicResource UncheckedItemDataTemplate}"/>
    <ListBox Grid.Row="2" Margin="5" Grid.Column="1" ItemsSource="{Binding CheckedItems}" ItemTemplate="{DynamicResource CheckedItemDataTemplate}"/>

This is a cleaner solution, hope is helps.

Raúl Otaño
  • 4,640
  • 3
  • 31
  • 65
  • I do have one question about this. Can the ItemTemplate, instead of being statically defined, be a binding and still follow MVVM? – phillk6751 Mar 26 '15 at 13:53
  • 1
    The use of the ItemTemplate is variable, I'm sure that you can use a binding for this. The way I like most is using the x:Type: in this way you can define the data template in any parent resource dictionary, and it will be applied for every item of that type... – Raúl Otaño Mar 26 '15 at 14:47
  • Perfect, I used ItemsControl.Resources to define multiple DataTemplates, and then set the DataContext association by using DataType="{x:Type VMName}" – phillk6751 Mar 26 '15 at 18:44