ListBox Index returns negative value - windows-phone-7

I'm creating a windows phone app using MVVM light pattern. I'm having trouble on my list box as it always returning a negative value (-1) for selected index. Does anyone knows how to resolve it?
here is my code in View Model, do i missed anything?Thanks!
public void OnViewListSelectedItem(SelectionChangedEventArgs e)
{
ListBox lb = new ListBox();
if (e.AddedItems.Count == 1)
{
if (lb.SelectedIndex == 0)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByVendorUrl, UriKind.Relative));
}
if (lb.SelectedIndex == 1)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByVendorUrl, UriKind.Relative));
}
if (lb.SelectedIndex == 2)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByCombinationUrl, UriKind.Relative));
}
}
}
XAML code Here
<ListBox x:Name="lbviewlist">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<Command:EventToCommand Command="{Binding ViewListCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.Items>
<ListBoxItem Content="By Product" FontSize="35" Margin="10,12,12,0"/>
<ListBoxItem Content="By Vendor" FontSize="35" Margin="10,12,12,0"/>
<ListBoxItem Content="By Best Combination" FontSize="35" Margin="10,12,12,0"/>
</ListBox.Items>
</ListBox>

You are creating a new ListBox() (called lb) in your code. You don't populate it, so it will be empty and will always have a SelectedIndex of -1
Then check the 'Source' property of 'e' and cast it to a ListBox
ListBox myList = (ListBox) e.Source;
You can then access the properties on myList.

based on my research SelectedIndex property of listbox is not bindable, when you use the get accessor for the SelectedIndex property, it always returns -1. An attempt to use the set accessor for the SelectedIndex property raises a NotSupportedException. -- MSDN List selectedporperty
I also updated my code since my first code is wrong that creates new listbox and that results to empty/null. Also selectionchanged event does not have a problem to be used as event.
public void method (SelectionChangedEventArgs e)
{
{
if (e.AddedItems.Count == 1)
{
var listBoxItem = (ListBoxItem)e.AddedItems[0];
string _string1 = "Test";
if ((string)listBoxItem.Content == _string1)
{
navigationService.NavigateTo(new Uri(ViewModelLocator.page1, UriKind.Relative));
}
}
}
}
Thats it. Hope it helps! :)

Related

Xamarin Forms ListView cancel ItemSelected event

With the Xamarin Forms ListView, the ItemSelected event is fired each time an element is selected in the list.
Is there a way to cancel out of this event so that the new item isn't selected and the old item remains selected?
The use case is a master/detail type of view where selecting an item in the list changes the detail. But the detail view from the previous selection might have been altered and the user needs to decide to discard or save the previous changes before changing the List's SelectedItem.
#SushiHangover's suggestion to control the SelectionMode property and disable/enable the selection of the ListView is a good one. However, I have an alternate solution that will revert the ListView's selected item to the previous item for anyone who might have a similar need.
I will only post snippets of the solution, but they should be complete enough for someone else to learn and implement.
First, I am using FreshMVVM which provides (amongst many things), essentially, syntactic sugar over binding the View to the ViewModel. Also, the PropertyChanged nuget package creates the INotifyPropertyChanged boilerplate code at compile time. That is why you don't see the familiar XF patterns you normally see with that interface. AddINotifyPropertyChanged handles all that.
The solution to my problem is a dedicated, generic ListViewModel that can be bound to any ListView that needs the ability "roll back" a selection changed event. It binds to the Items collection. Additionally the SelectedItem property is bound to the control as well.
The constructor takes a Func which is called to determine if it's ok to move the selection or not.
[AddINotifyPropertyChangedInterface]
public class ListViewModel<T>
{
private Func<bool> _beforeChangeValidator;
private Action _afterChange;
public ListViewModel(Func<bool> beforeChangeValidator, Action afterChange)
{
_beforeChangeValidator = beforeChangeValidator;
_afterChange = afterChange;
_changing = false;
}
public int SelectedIndex { get; set; }
public T SelectedItem { get; set; }
public ObservableCollection<T> Items { get; set; }
private bool _changing;
public Command SelectedItemChanged
{
get
{
return new Command((args) =>
{
if (!_changing)
{
if (_beforeChangeValidator())
{
SelectedIndex = ((SelectedItemChangedEventArgs)args).SelectedItemIndex;
}
}
_changing = false;
});
}
}
public void RevertSelectedItemChanged()
{
_changing = true;
SelectedItem = Items[SelectedIndex];
}
}
And the code in the parent ViewModel has the Func (TagListBeforeChange) that determines if it's ok to move the selection or not. In this case I am checking if the last selected item has been changed, and if it has, prompt the user for what to do.
public override void Init()
{
TagListViewModel = new ListViewModel<Tag>(TagListBeforeChange, null);
}
private bool TagListBeforeChange()
{
if (ActiveTag.HasChanged)
{
var confirmConfig = new ConfirmConfig()
{
Message = "Current tag has changed. Discard changes and continue?",
OkText = "Discard Changes",
CancelText = "Cancel",
OnAction = (result) =>
{
if (result)
{
_mapper.Map(TagListViewModel.SelectedItem, ActiveTag);
}
else
{
TagListViewModel.RevertSelectedItemChanged();
}
}
};
_userDialogs.Confirm(confirmConfig);
return false;
}
_mapper.Map(TagListViewModel.SelectedItem, ActiveTag);
return true;
}
And finally, here is the ListView control declaration...
<ListView ItemsSource="{Binding TagListViewModel.Items}"
SelectedItem="{Binding TagListViewModel.SelectedItem, Mode=TwoWay}">
<ListView.Behaviors>
<behaviors:EventHandlerBehavior EventName="ItemSelected">
<behaviors:InvokeCommandAction Command="{Binding TagListViewModel.SelectedItemChanged}" />
</behaviors:EventHandlerBehavior>
</ListView.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView Padding="8">
<Label Text="{Binding DisplayValue}" />
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

How are databound views rendered?

When a Windows Phone 7 application opens a view, a certain order of business is followed in order to create. As far as constructors and events go, I have found this order to be true:
Constructor
OnNavigatedTo
OnLoaded
However, I am in a position where I need to databind a List to a ListBox after the basic view (background, other elements etc) has loaded. So I need to know when and how to know that the view is loaded before I get on with the data binding.
I have tried to do this on the OnLoaded-event, but it seems like if I do the data binding here - and right after it traverse those elements - they don't seem to exist yet (the VisualTreeHelper-class can't seem to find the nodes). So as you see, I am stuck.
Any help would be greatly appreciated.
Edit: As requested, here is some more information about what's going on.
My List is populated by some custom (not too complicated) objects, including an asynchronously loaded image (courtesy of delay.LowProfileImageLoader) and a rectangle.
The XAML:
<ListBox x:Name="ChannelsListBox" ItemsSource="{Binding AllChannels}">
//...
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="ChannelTile" Margin="6,6,6,6" Tap="ChannelTile_Tap" Opacity="0.4">
<!-- context menu goes here -->
<Rectangle Width="136" Height="136" Fill="{StaticResource LightGrayColor}" />
<Image Width="136" Height="136" delay:LowProfileImageLoader.UriSource="{Binding ImageUri}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The code-behind:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
UpdateApplicationBar();
pickChannelsViewModel = new PickChannelsViewModel();
DataContext = pickChannelsViewModel;
if (hasUpdatedTiles)
{
pickChannelsViewModel.IsLoading = false; // Set by UpdateTiles()
}
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
// This is where I would data bind the list (instead of in XAML)
UpdateTiles(); // Traverses the list and changes opacity of "selected" items.
}
protected void UpdateTiles()
{
foreach (var item in ChannelsListBox.Items)
{
if (pickChannelsViewModel.SelectedChannels.Contains(item as Channel))
{
var index = ChannelsListBox.Items.IndexOf(item);
// This returns null when databinding in codebehind,
// but not in XAML
ListBoxItem currentItem = ChannelsListBox.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
if (currentItem != null && VisualTreeHelper.GetChildrenCount(currentItem) == 1)
{
var OuterWrapper = VisualTreeHelper.GetChild(currentItem, 0);
var MiddleWrapper = VisualTreeHelper.GetChild(OuterWrapper, 0);
var InnerWrapper = VisualTreeHelper.GetChild(MiddleWrapper, 0);
Grid currentItemGrid = VisualTreeHelper.GetChild(InnerWrapper, 0) as Grid;
currentItemGrid.Opacity = 1.0;
}
}
}
pickChannelsViewModel.IsLoading = false;
hasUpdatedTiles = true;
}
The items themselves are in-memory (fetched from REST at an earlier stage in the application), so should be available instantaneously.
The issue I am trying to resolve is a fairly long load time on this particularly view (there is about 140 of these items being created, then filtered through and changing the opacity).
I believe you are doing something like:
myListBox.ItemSource=myList;
Once you set the ItemSource of a ListBox the changes in your List should be reflected in the ListBox at all times. If the ListBox is empty the reason must be that the List is not being populated properly or invalid Bindings in the ItemTemplate. You should debug and find out if your List has any items by inserting a breakpoint in the Loaded() method. Also, you've not mentioned what items does your List contains or, where is it being populated in the application? Incomplete information doesn't help anyone.

Using ListPicker and DataBinding

Ok. I give up.
I want to use ListPicker control in one of my Windows Phone apps. I am getting an Exception SelectedItem must always be set to a valid value.
This is my XAML piece of ListPicker:
<toolkit:ListPicker x:Name="CategoryPicker"
FullModeItemTemplate="{StaticResource CategoryPickerFullModeItemTemplate}"
Margin="12,0,0,0"
ItemsSource="{Binding CategoryList}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
ExpansionMode="ExpansionAllowed"
FullModeHeader="Pick Categories"
CacheMode="BitmapCache"
Width="420"
HorizontalAlignment="Left" />
CategoryList is an ObservableCollection<Category> in my ViewModel.
SelectedCategory is a property in my ViewModel of type Category.
This is how I am declaring both CategoryList and SelectedCategory:
private Category _selectedCategory;// = new Category();
private ObservableCollection<Category> _categoryList = new ObservableCollection<Category>();
public ObservableCollection<Category> CategoryList
{
get
{
return _categoryList;
}
set
{
_categoryList = value;
RaisePropertyChanged("CategoryList");
}
}
public Category SelectedCategory
{
get
{
return _selectedCategory;
}
set
{
if (_selectedCategory == value)
{
return;
}
_selectedCategory = value;
RaisePropertyChanged("SelectedCategory");
}
}
Appreciate your help!!! Maybe I have not understood the usage of ListPicker very well.
I'd expect the object returned by SelectedCategory to be one of the objects from the CategoryList collection. In your example you are instanciating it within the get, so this is definitely not the case.
If CategoryList contains some values, then perhaps initialize _selectedCategory to null, and then in the get
if(_selectedCategory == null) {
_selectedCategory = CategoryList.First();
}
Take a look at my answer to this question:
Silverlight ComboBox binding with value converter
The short answer is that the selected item must be an item that is contained within the collection. Your getter is setting the selected item to a new object. This new object is not contained within the collection

Correct way to use a Interaction Trigger on SelectionChanged Event

I have a command wired to the event such that it does fire, but what I get in the CommandParameter is the previously selected item, or maybe it's the selected item before the SelectionChanged completes.
Either way, not sure what to change to get the newly selected item from the event.
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand
Command="{Binding Main.SelectedRecordCommand, Source={StaticResource Locator}}"
CommandParameter="{Binding SelectedItem, ElementName=listBillingRecords}"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
Thanks
Is it worth using a trigger? If whatever your XAML element is for the collection (listbox, grid, etc) is bound to a property exposing a collection on your viewmodel, you can leverage both databinding and the built-in MVVM Light messenger to notify you of a property change with both old and new values in a more MVVM-friendly way. This example isn't necessarily WP7-specific, but I think it would work the same.
For example, this might be the databound collection:
public const string BillingRecordResultsPropertyName = "BillingRecordResults";
private ObservableCollection<BillingRecord> _billingRecordResults = null;
public ObservableCollection<BillingRecord> BillingRecordResults
{
get
{
return _billingRecordResults;
}
set
{
if (_billingRecordResults == value)
{
return;
}
var oldValue = _billingRecordResults;
_billingRecordResults = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(BillingRecordResultsPropertyName, oldValue, value, true);
}
}
I like to expose a property on my ViewModel that is a "selected item" of whatever collection I'm exposing. So, to the ViewModel, I would add this property using the MVVMINPC snippet:
public const string SelectedBillingRecordPropertyName = "SelectedBillingRecord";
private BillingRecord _selectedBillingRecord = null;
public BillingRecord SelectedBillingRecord
{
get
{
return _selectedBillingRecord;
}
set
{
if (_selectedBillingRecord == value)
{
return;
}
var oldValue = _selectedBillingRecord;
_selectedBillingRecord = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(SelectedBillingRecordPropertyName, oldValue, value, true);
}
}
Now, if you bind the SelectedItem of the XAML element to this exposed property, it will populate when selected in the View via databinding.
But even better, when you leverage the snippet MVVMINPC, you get to choose whether or not to broadcast the results to anyone listening. In this case, we want to know when the SelectedBillingRecord property changes. So, you can have this in the constructor for your ViewModel:
Messenger.Default.Register<PropertyChangedMessage<BillingRecord>>(this, br => SelectedRecordChanged(br.NewValue));
And elsewhere in your ViewModel, whatever action you want to have happen:
private void SelectedRecordChanged(BillingRecord br)
{
//Take some action here
}
Hope this helps...
I have seen the same issue and found that SelectedItem is the correct implementation.

What is the proper way to perform page navigation on ListBox selection changes

I'm trying the MVVM Light Toolkit. Though I still think having multiple ViewModels for such small apps is overkill, I like the concepts. What I still can't quite understand is how (or I should say "what is the recommended way") to navigate from one page to another when the selection changes in a ListBox.
The big problem with this toolkit is that it forces you to learn MVVM via other sources before using it, rather than show you what (its vision of) MVVM is from within the framework, accompanying samples and documentation. Are there samples out there showing the different concepts? And please, no videos.
Have you tried modifying your ListBox ItemTemplate to have each item be a HyperlinkButton and just setting the NavigateURI attribute to the Page you want to navigate to?
I still have not figured out how to do this (navigate to a details page upon selection changed in a listbox) without any codebehind in the view. However, if you are OK with having just a little codebehind in the view here's what I recommend:
<ListBox x:Name="MainListBox" Margin="0,0,-12,0" ItemsSource="{Binding Items}"
SelectionChanged="MainListBox_SelectionChanged"
SelectedItem="{Binding Path=SelectedListItem, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432">
<TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
<TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
First, per above bind to the SelectedItem property of the Listbox with a TwoWay binding to a property in your ViewModel (SelectedListItem in the above).
Then in your codebehind for this page implement the handler for MainListBox_SelectionChanged:
// Handle selection changed on ListBox
private void MainListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// If selected index is -1 (no selection) do nothing
if (MainListBox.SelectedIndex == -1)
return;
// Navigate to the new page
NavigationService.Navigate(new Uri("/DetailsPage.xaml", UriKind.Relative));
}
This is the only codebehind you need in your main view.
In your main ViewModel you need a SelectedListItem property:
public const string SelectedListItemPropertyName = "SelectedListItem";
private ItemViewModel _SelectedListItem;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding
/// </summary>
/// <returns></returns>
public ItemViewModel SelectedListItem
{
get
{
return _SelectedListItem;
}
set
{
if (value != _SelectedListItem)
{
_SelectedListItem = value;
RaisePropertyChanged(SelectedListItemPropertyName);
}
}
}
Now, the trick to getting the context passed to your details page (the context being what list item was selected) you need to setup the DataContext in your Details view:
public DetailsPage()
{
InitializeComponent();
if (DataContext == null)
DataContext = App.ViewModel.SelectedListItem;
}
Hope this helps.
eventually you'll want to do more than just navigate, potentially navigate after setting a custom object.
Here is a MVVM-light way of doing this.
You'll first want to bind your listbox selected item to a property in your viewmodel
<ListBox ItemsSource="{Binding Events}" Margin="0,0,-12,0" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
Declare your SelectedEvent property
public const string SelectedEventPropertyName = "SelectedEvent";
private Event _selectedEvent;
public Event SelectedEvent
{
get {return _selectedEvent;}
set
{
if (_selectedEvent == value)
{
return;
}
var oldValue = _selectedEvent;
_selectedEvent = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(SelectedEventPropertyName, oldValue, value, true);
}
}
You can then define an interaction trigger bound to the tap event
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap">
<cmd:EventToCommand Command="{Binding EventPageCommand, Mode=OneWay}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In your viewmodel, define your EventPageCommand as a RelayCommand:
public RelayCommand EventPageCommand { get; private set; }
public MainViewModel()
{
EventPageCommand = new RelayCommand(GoToEventPage);
}
and finally declare your GoToEventPage method
private void GoToEventPage()
{
_navigationService.NavigateTo(new Uri("/EventPage.xaml", UriKind.Relative));
}
note that you can do other actions before navigating to your new page, plus your selected item from your list box is currently set in the property you bound it too.

Resources