0

I have a viewmodel ProductsViewModel, and one of the methods adds a new Product to the ProductList that it stores. I currently have a ListBox bound to the ProductList. I add a new product by having a button bound to a simple Command which calls the relevant method on the viewmodel.

How do I modify the view to select the new Product that's been added to the ListBox and scroll down to the new item when the view model can't 'talk' to the view?

Edit

Note, I do not want the last item to be automatically selected every time a new item is added to the listbox, because that will select the last item when I import items to the listbox which I want to avoid.

Community
  • 1
  • 1
user3791372
  • 4,445
  • 6
  • 44
  • 78
  • Do you raise the `PropertyChanged` event of the ViewModel? – Aly Elhaddad Jan 14 '16 at 21:24
  • @AlyEl-Haddad yes though for hte product list, I'm using an ObservableCollection – user3791372 Jan 14 '16 at 21:26
  • 1
    Possible duplicate of [mvvm how to make a list view auto scroll to a new Item in a list view](http://stackoverflow.com/questions/3317194/mvvm-how-to-make-a-list-view-auto-scroll-to-a-new-item-in-a-list-view). The correct MVVM way of doing this is by way of a behavior as shown in the accepted answer to that question. – Mark Feldman Jan 15 '16 at 04:32
  • @MarkFeldman not a duplicate as that references automatic scrolling where I want to be able to trigger it at certain points only. – user3791372 Jan 15 '16 at 17:42
  • @user3791372 ok that's a very different question to the one you asked though, see my answer below. – Mark Feldman Jan 15 '16 at 21:32

2 Answers2

3

Create a property in your ViewModel 'SelectedProduct' (obviously it will need to raise property changed. After you add a new product to the ProductList, also update SelectedProduct with this new product. In the View, bind ListBox's SelectedItem to CurrentProduct.

denis morozov
  • 6,236
  • 3
  • 30
  • 45
  • So, the viewmodel should be responsible for setting the current selected item on the listbox? – user3791372 Jan 14 '16 at 22:30
  • I think so. That's how everyone does it, as far as I know. – denis morozov Jan 14 '16 at 23:06
  • 1
    in terms of mvvm, the viewmodel shoudln't know about the view. – user3791372 Jan 14 '16 at 23:23
  • Hmm, I don't think that we are breaking MVVM. Lets just say this was a poor naming convention, how about instead of SelectedProduct - CurrentProduct. Assuming VM needs to know what a current product is, how it is set by the view is irrelevant (or unknown) hense separation between VM and the View. This is a common practice to set a CurrentItem in the VM in order to maybe edit it, save it to db, etc. Maybe VM sets the CurrentProduct, and the view has ability to react to it through binding. – denis morozov Jan 15 '16 at 16:29
0

In general the best way to achieve this is with a behaviour. The implementation will likely depend on your specific requirements but what I'll provide here is a generic example that shows how to make the view model trigger the ListBox to scroll to a specific item of your choosing.

First of all you need a way of communicating this from the view model to the view, you can't bind directly to events in XAML but you can encapsulate the event in a wrapper and bind to that instead:

public class ListBoxScrollHandler
{
    public event Action<object> ScrollEvent;

    public void ScrollTo(object item)
    {
        if (this.ScrollEvent != null)
            this.ScrollEvent(item);
    }
}

That class contains an event which our behaviour can bind to and a ScrollTo method that our view model can invoke. For the view let's just create a simple listbox that we'll fill with numbers (actually strings) and a button that will force us to scroll to the element with the content "500":

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition />
    </Grid.RowDefinitions>

    <Button Content="Scroll to 500" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{Binding ScrollCommand}" CommandParameter="500" />
    <ListBox Grid.Row="1" ItemsSource="{Binding MyItems}" SelectedItem="{Binding CurrentItem}" ScrollViewer.VerticalScrollBarVisibility="Visible">
        <i:Interaction.Behaviors>
            <behaviors:ListBoxScrollBehavior ScrollHandler="{Binding ScrollHandler}" />
        </i:Interaction.Behaviors>
    </ListBox>
</Grid>

As you can see I've implemented this with a Blend behaviour, you can of course do it with a regular attached behaviour if you want but I'm keeping things simple here:

public class ListBoxScrollBehavior : Behavior<ListBox>
{
    public ListBoxScrollHandler ScrollHandler
    {
        get { return (ListBoxScrollHandler)GetValue(ScrollHandlerProperty); }
        set { SetValue(ScrollHandlerProperty, value); }
    }

    public static readonly DependencyProperty ScrollHandlerProperty =
        DependencyProperty.Register("ScrollHandler", typeof(ListBoxScrollHandler),
        typeof(ListBoxScrollBehavior), new PropertyMetadata(null, OnScrollHandlerChanged));

    protected override void OnAttached()
    {
        base.OnAttached();
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
    }

    private static void OnScrollHandlerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as ListBoxScrollBehavior;
        if (behavior == null)
            return;

        var oldHandler = e.OldValue as ListBoxScrollHandler;
        if (oldHandler != null)
            oldHandler.ScrollEvent -= behavior.ScrollTo;

        var newHandler = e.NewValue as ListBoxScrollHandler;
        if (newHandler != null)
            newHandler.ScrollEvent += behavior.ScrollTo;
    }

    public void ScrollTo(object item)
    {
        this.AssociatedObject.ScrollIntoView(item);
    }

}

So our behaviour contains a "ScrollHandler" dependency property that we can bind to our view model and responds by calling the listbox's ScrollIntoView method. After that it's simply a matter of creating a view model that provides this property along with code to initialize the list items and a command handler that responds to the button press and invokes it's scroll handler's ScrollTo method:

public class MainViewModel : ViewModelBase
{
    private ObservableCollection<string> _MyItems = new ObservableCollection<string>();
    public ObservableCollection<string> MyItems
    {
        get { return this._MyItems; }
        set { this._MyItems = value; RaisePropertyChanged(); }
    }

    private string _SelectedItem;
    public string SelectedItem
    {
        get { return this._SelectedItem; }
        set { this._SelectedItem = value; RaisePropertyChanged(); }
    }

    public ICommand ScrollCommand { get { return new RelayCommand<string>(OnScroll); } }
    private void OnScroll(string item)
    {
        this.ScrollHandler.ScrollTo(item);
    }

    private ListBoxScrollHandler _ScrollHandler = new ListBoxScrollHandler();
    public ListBoxScrollHandler ScrollHandler
    {
        get { return this._ScrollHandler;}
        set { this._ScrollHandler = value; RaisePropertyChanged(); }
    }

    public MainViewModel()
    {
        for (int i = 0; i < 1000; i++)
            this.MyItems.Add(i.ToString());
    }
}

Run the code, click the button and the listbox will scroll down to the element containing the "500" content. Obviously if you only need a sub-set of this behaviour (e.g. scroll to the currently selected item) then you can modify this behaviour accordingly.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58