Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVVM Listbox Update Content Maintain Selected Item Silverlight

I've been reading a lot about MVVM (using Laurent Bugnion's library in specific) and I'm constantly struggling to determine how to do things in MVVM that were otherwise easy with code behind.

Here's just one example where I suspect I'm doing things the hard way. If anyone has the time to read all this, perhaps they can comment on the sanity of my approach. :)

I have a list box bound to a ViewModel like so:

<ListBox x:Name="lstFruitBasketLeft" ItemsSource="{Binding FruitBasket}" 
     SelectedItem="{Binding SelectedFruit, Mode=TwoWay}"  Width="150">
<ListBox.ItemTemplate>
    <DataTemplate>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" 
                    HorizontalAlignment="Left" Margin="2">
            <TextBlock Text="{Binding Name}" />
            <TextBlock Text=":" />
            <TextBlock Text="{Binding Quantity}" />
        </StackPanel>
    </DataTemplate>
</ListBox.ItemTemplate>

The ItemSource is an ObservableCollection of Fruit objects:

public class Fruit
{
    public string Name { get; set; }
    public int Quantity { get; set; }

    public Fruit() { }
    public Fruit(string name, int quantity) 
    {
      this.Name = name;
      this.Quantity = quantity;
    }
  }

It is defined in the ViewModel as:

// Property FruitBasket
public const string FruitBasketPropertyName = "FruitBasket";
private ObservableCollection<Fruit> _fruitBasket = null;
public ObservableCollection<Fruit> FruitBasket
{
  get { return _fruitBasket; }
  set
  {
    if (_fruitBasket == value)
      return;

    _fruitBasket = value;

    // Update bindings, no broadcast
    RaisePropertyChanged(FruitBasketPropertyName);
  }
}

The bound SelectedItem property is as such:

//Property SelectedFruit
public const string SelectedFruitPropertyName = "SelectedFruit";

private Fruit _selectedFruit = null;

public Fruit SelectedFruit
{
  get { return _selectedFruit; }
  set
  {
    if (_selectedFruit == value)
      return;

    var oldValue = _selectedFruit;
    _selectedFruit = value;

    // Update bindings, no broadcast
    RaisePropertyChanged(SelectedFruitPropertyName);
  }
}

Then, the list is populated on the construction of the ViewModel.

Now, I add a RelayCommand to a button on the presentation page that executes a method which increments the quantity of the selected item. Note that I am not using the parameter yet, but "Bob" is a placeholder for some changes for later.

<Button x:Name="butMore" Content="More!" HorizontalAlignment="Right" Height="25" Width="75" Margin="4">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <cmd:EventToCommand
                Command="{Binding addMoreCommand}"
                CommandParameter="Bob" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

Here's the code for the command:

// Property addMoreCommand
public RelayCommand addMoreCommand
{
  get;
  private set;
}

...

  //Init relays (this is in the constructor)
  addMoreCommand = new RelayCommand(AddFruit, CanExecute);

...

public void AddFruit()
{
  //Increment the fruit
  SelectedFruit.Quantity++;

  //Save the previous selected item
  Fruit oldSelectedItem = SelectedFruit;

  //We have to have a new list in order to get the list box to refresh
  FruitBasket = new ObservableCollection<Fruit>(FruitBasket);

  //Reselect
  SelectedFruit = oldSelectedItem;
}


public bool CanExecute()
{
  return true; //for now
}

Now this does work, but I have some problems with it:

First, I feel like there are a lot of conditions that have to come together for this to work and I wonder if I'll get so lucky trying to move some Telerik Drag and Drop code into MVVM.

Second, it seems like a pretty poor performance approach to recreate the list like that.

Lastly, it seems like this would be easier in code behind (though I'm not 100% certain I still won't have to rebuild that list).

Does anyone have any thoughts on my approach or perhaps even... suggestions to make things easier? Am I just missing something obvious here?

Thanks

-Driodilate :]

like image 690
maulkye Avatar asked Nov 18 '10 19:11

maulkye


1 Answers

maulkye,

There is something going wrong if you have to refresh your ObservableCollection. Usually, you should not need it because the ObservableCollection will notify about item changes.

Never do this:

FruitBasket = new ObservableCollection<Fruit>(FruitBasket);

Your public ObservableCollection<Fruit> FruitBasket should have no public setter, it should be read only. Just Add or Remove Items to/from the list.

If you want to handle multiple selections, you will probably need an extended CollectionView which can handle this, get more hints here.

I hope this helps a little bit, even if I probably didn't answer all questions : )

EDIT: Ok, I guess i got some things wrong. Now i guess i fully understand what you're trying to accomplish. You are not getting notified when your property is changed, right? Well, for this reason, we've adapted "BindableLinq" in one of our projects, which you can compile in Silverlight without problems. (there are similar solutions available, called Continuous Linq or Obtics, make your choice).

Using BindableLinq, you can transform your ObservableCollection to a BindableCollection using one single extension method. The BindableCollection will then reflect all changes properly. Give it a try.

EDIT2: To implement a proper ViewModel, Please consider the following Changes.

1) Fruit is your Model. Since it doesn't implement INotifyPropertyChanged, it won't propagate any changes. Create a FruitViewModel, embedding your Fruit Model and invoke RaisePropertyChanged for each property setter.

2) Change your FruitBasket to be an ObservableCollection of FruitViewModel. Slowly it starts to make sense :)

3) SelectedFruit has to be a FruitViewModel as well. Now it makes even more sense.

4) Now it already works for me, even without BindableLinq. Did you have any success?

HTH

best regards,
thomas

like image 184
thmshd Avatar answered Nov 15 '22 13:11

thmshd