SelectedItem must always be set to a valid value - mvvm

I have two viewmodel, on the first viewmodel i have a listbox:
<ListBox x:Name="MainMenu" toolkits:TiltEffect.IsTiltEnabled="True"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
ItemTemplate="{StaticResource MainMenu}"
ItemsSource="{Binding Categories}" Margin="0,97,0,0"
Tap="MainMenu_Tap">
In the second page, i have a listpicker
<toolkit:ListPicker Margin="0,153,0,0" Background="{StaticResource PhoneAccentBrush}" VerticalAlignment="Top"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding Item}"
ItemTemplate="{StaticResource CategorySelector}"
FullModeHeader="Category"
FullModeItemTemplate="{StaticResource FullCategorySelector}"
BorderBrush="{StaticResource PhoneAccentBrush}"/>
What i want is when I navigate to second page, the selected item in the first page will be selected in the second page. But I always get the selected item must always set to a valid value when I navigate to second page.
first viewmodel
private CategoryModel _selectedItem = null;
public CategoryModel SelectedItem
{
get { return _selectedItem; }
set
{
if (_selectedItem == value)
{
return;
}
var oldValue = _selectedItem;
_selectedItem = value;
RaisePropertyChanged("SelectedItem", oldValue, value, true);
}
}
second viewmodel
private CategoryModel _item = null;
public CategoryModel Item
{
get { return _item; }
set
{
if (_item == value)
{
return;
}
var oldValue = _item;
_item = value;
// Update bindings, no broadcast
RaisePropertyChanged("Item");
}
}
EDIT
When I change the listpicker in the second page to Listbox, it works pretty well.
So this is an issue enter link description here. How should I do to get this thing work with the listpicker?

I think you're confusing views and viewmodels.
Because you're binding the selected item in XAML, when the XAML is parsed and the page created it's trying to bind to an item in a collection which hasn't been created yet. This is why the comments on the bug suggest a work around when setting this in code behind.
In your Tap handler on the first page, I assume that you're passing some details of the selected item to the second page. You could, therefore, remove the XAML binding of the selected item and in the OnNavigatedTo event handler on the second page set the binding in code, once you know the ItemsSource has been populated.
Alternatively, you could consider having the two pages share the same viewmodel instance.

ListPicker uses Items.IndexOf to get the index of item instance that should select.
If the instance does not match (it is not an object instance from the collection) the IndexOf will return -1 and the InvalidOperationException is thrown with the message: "SelectedItem must always be set to a valid value".
Override Equals method of the type in the collection and it will work as expected.
Example:
public override bool Equals(object obj)
{
var target = obj as ThisTarget;
if (target == null)
return false;
if (this.ID == target.ID)
return true;
return false;
}
Hope it helps

Related

Make focus go to next entry in a collection view

I have an application in .Net Maui that uses a collection view with an entry field and after the collection view one static entry field. If you are currently focused on the first entry in the collection view and hit tab or enter it will not navigate to the next entry in the collection view and focus on the static entry field. I need to find the best way to have the entry focus on the next entry in the collection view on complete.
I have tried changing the return type of the collection view entry field to Next and also tried the community toolkit SetFocusOnEntryCompletedBehavior function and both result in the same behavior of not navigating to the next entry from the collection view. Very similar to this issue that doesnt seem to be resolved. MAUI - CollectionView jump / focus to next entry
I found a workaround for you. You could try the following code:
Step1 Create a custom control , let's call it MyEntry (MyEntry.cs) which subclass Entry:
In this control we attach a BindableProperty IsExpectedToFocusProperty which we used it to judge whether it is goning to be focused. We also registered a new method OnIsExpectedToFocus to detect propertyChanged for our control. For info about BindableProperty, you could refer to Bindable properties.
MyEntry.cs,
public class MyEntry : Entry
{
public static readonly BindableProperty IsExpectedToFocusProperty = BindableProperty.Create("IsExpectedToFocus", typeof(bool), typeof(MyEntry), false, propertyChanged:OnIsExpectedToFocus);
public bool IsExpectedToFocus
{
get => (bool)GetValue(IsExpectedToFocusProperty);
set => SetValue(IsExpectedToFocusProperty, value);
}
static void OnIsExpectedToFocus(BindableObject bindable, object oldValue, object newValue)
{
// Property changed implementation goes here
if ((bool)newValue == true)
{
(bindable as Entry).Focus();
}
}
}
Step2 Consume custom control in CollectionView. We define the ReturnCommand and its parameter. we will bind them in the MainPageViewModel.
MainPage.xaml,
<CollectionView x:Name="mycoll" ItemsSource="{Binding ItemCollection}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<local:MyEntry x:Name="myentry" Focused="myentry_Focused"
IsExpectedToFocus="{Binding IsExpectedToFocus}"
Text="{Binding Title,Mode=TwoWay}" TextColor="Black"
ReturnCommand="{Binding Source={RelativeSource AncestorType={x:Type local:MainPageViewModel}}, Path=ReturnCommand}"
ReturnCommandParameter="{Binding .}"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
In .cs file:
void myentry_Focused(System.Object sender, Microsoft.Maui.Controls.FocusEventArgs e)
{
var entry = sender as Entry;
foreach (var item in viewModel.ItemCollection)
{
if (entry.BindingContext != item)
{
item.IsExpectedToFocus = false;
}
}
}
Step3 Design our MainPageViewModel. I define an ObservableCollection which ItemSource will bind to. And add three items just for test.
Then I think the most important part is to design the Command. Let me explain it briefly. When we press the entry of an Entry, we fire the ReturnCommand and get current Item through ReturnCommandParameter. We get the index of current Item in ItemCollection. So the next entry which needs to be focused corresponds to the index+1 Item. Then we changed the IsExpectedToFocus of the next entry and fire the OnIsExpectedToFocus method which set the entry be focused. Done!
MainPageViewModel.cs
public class MainPageViewModel
{
public ObservableCollection<Item> ItemCollection { get; set; } = new ObservableCollection<Item>();
public Command ReturnCommand
{
get
{
return new Command<Item>((e) =>
{
e.IsExpectedToFocus = false;
int index = ItemCollection.IndexOf(e); // get the current index
if (index != -1)
{
int nextIndex;
// if last entry, next index is 0, else index +1
if (index < (ItemCollection.Count() - 1))
{
nextIndex = index + 1;
ItemCollection[nextIndex].IsExpectedToFocus = true;
}
else if(index == (ItemCollection.Count() - 1))
{
nextIndex = 0;
ItemCollection[nextIndex].IsExpectedToFocus = true;
}
}
});
}
}
public MainPageViewModel()
{
//add three item for test
ItemCollection.Add(
new Item
{
Title = "12345",
IsExpectedToFocus = false
}) ;
ItemCollection.Add(
new Item
{
Title = "23456",
IsExpectedToFocus = false
});
ItemCollection.Add(
new Item
{
Title = "34567",
IsExpectedToFocus = false
});
}
}
Also, this is Item.cs, should implement INotifyPropertyChanged
public class Item : INotifyPropertyChanged
{
public string title;
public bool isExpectedToFocus;
public string Title
{
get
{
return title;
}
set
{
title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
public bool IsExpectedToFocus
{
get
{
return isExpectedToFocus;
}
set
{
isExpectedToFocus = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpectedToFocus)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Hope it works for you.

Use PopupExtensions.ShowPopupAsync function in Custom Control in MAUI

I created a custom control in MAUI that must work if user select with a click or tap, a Popup must show with some content, let's say for example a Calculator instead a Keyboard. I'm using CommunityToolkit.Maui. But the sentence
var popup = new PickerControl();
var result = await PopupExtensions.ShowPopupAsync<PickerControl>(this, popup);
throw me an error because this in inside the control and expects a Page, so need to know how handle the page or parent page in the same control. Picker control is the Popup with the content.
The code:
public partial class EntryCalculator : Frame
{
TapGestureRecognizer _tapGestureRecognizer;
public EntryCalculator()
{
InitializeComponent();
}
///Properties here
private void Initialize()
{
_tapGestureRecognizer = new TapGestureRecognizer();
}
private async static void IsDisplayPickerPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var controls = (EntryCalculator)bindable;
if (newValue != null)
{
if ((bool)newValue)
{
var popup = new PickerControl();
var response = PopupExtensions.ShowPopupAsync<PickerControl>(this, popup);
if (response != null && response is decimal)
{
controls.Value = (decimal)response;
}
}
}
}
///... other methods
At first, you can get the current page from the navigation stack:
If you use the shell:
Page currentpage = Shell.Current.Navigation.NavigationStack.LastOrDefault();
If you use the NavigationPage:
Page currentpage = Navigation.NavigationStack.LastOrDefault();
Or just only use:Page currentpage = App.Current.MainPage.Navigation.NavigationStack.LastOrDefault();. The App.Current.MainPage will be the Shell or the NavigationPage, it depends on what you used in your project.
In addition, you can get the current page from the custom control. Such as:
public static class ViewExtensions
{
/// <summary>
/// Gets the page to which an element belongs
/// </summary>
/// <returns>The page.</returns>
/// <param name="element">Element.</param>
public static Page GetParentPage (this VisualElement element)
{
if (element != null) {
var parent = element.Parent;
while (parent != null) {
if (parent is Page) {
return parent as Page;
}
parent = parent.Parent;
}
}
return null;
}
}

What is the right usage for the SingleSelectionModel?

we would like to link from a CellTable to a property editor page. We use the SingleSelectionModel to get notified, when a user clicks on an item.
It is initialized like this:
private final SingleSelectionModel<Device> selectionModel = new SingleSelectionModel<Device>();
We then assign the selection change handler:
selectionModel.addSelectionChangeHandler(this);
Our selection change handler looks like this:
#Override
public void onSelectionChange(SelectionChangeEvent event) {
Log.debug("DevicesPresenter: SelectionChangeEvent caught.");
Device selectedDevice = selectionModel.getSelectedObject();
if (selectedDevice != null) {
selectionModel.clear();
if (selectionModel.getSelectedObject() != null){
Log.debug("DevicesPresenter: selected item is " + selectionModel.getSelectedObject());
}
else{
Log.debug("DevicesPresenter: selected item is null");
}
deviceEditorDialog.setCurrentDevice(selectedDevice.getUuid());
// get the container data for this device
clientModelProvider.fetchContainersForDevice(selectedDevice.getUuid());
PlaceRequest request = new PlaceRequest.Builder()
.nameToken(NameTokens.deviceInfo)
.with("uuid", selectedDevice.getUuid())
.build();
Log.debug("Navigating to " + request.toString());
placeManager.revealPlace(request);
}
}
Now there are two issues: There always seem to be two SelectionChangeEvents at once and i really cannot see why. The other thing is: How is the right way do handle selection of items and the related clearing of the selection model? Do we do that the right way?
Thanks!
If you only want to get notified of "clicks" without keeping the "clicked" item selected, use a NoSelectionModel instead; no need to clear the selection model as soon as something is selected.
As for your other issue with being called twice, double-check that you haven't added your selection handler twice (if you can unit-test your DevicesPresenter, introspect the handlers inside the selection model for example)
In your line selectionModel.addSelectionChangeHandler(this); what does this refer?
Here my code how I use SingleSelectionModel
public class MyClass{
private final SingleSelectionModel<CountryDto> selectionModel = new SingleSelectionModel<CountryDto>();
...
public MyClass(){
cellTable.setSelectionModel(selectionModel);
selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
#Override
public void onSelectionChange(SelectionChangeEvent event) {
CountryDto selected = selectionModel
.getSelectedObject();
if (selected != null) {
Window.alert("Selected country "+selected.getTitle());
}
}
});
}
}

MVVM SelectionChanged Combobox doesn't call 'Set'

my problem is that I want to call the 'SelectionChanged' Event in my ViewModel.
I have a ComboBox (here called ListPicker, it's a phone application):
<tool:ListPicker Name="txt_LZZ"
ItemsSource="{Binding ZZR}" SelectedItem="{Binding MySelectedItem}" />
My Property in the ViewModel looks like this:
private List<string> _zzr;
public List<string> ZZR
{
get
{
_zzr = new List<string>();
_zzr.Add("Jahr");
_zzr.Add("Monat");
_zzr.Add("Woche");
_zzr.Add("Tag");
return _zzr;
}
set
{
_zzr = value;
RaisePropertyChanged(() => ZZR);
}
}
private string _mySelectedItem;
public string MySelectedItem
{
get
{
return _mySelectedItem;
}
set
{
if (value == _mySelectedItem)
return;
_mySelectedItem = value;
RaisePropertyChanged(() => MySelectedItem);
GetValues();
}
}
The program only calls the get method once, while _mySelectedItem has the value 'null'. What I want is that when I change the SelectedItem in my Combobox (ListPicker), the ViewModel has to call the method GetValues, which is in my setter for MySelectedItem. Problem: -> ViewModel doesn't call setter. Why?
Try setting the mode of the binding to TwoWay:
<tool:ListPicker Name="txt_LZZ"
ItemsSource="{Binding ZZR}" SelectedItem="{Binding MySelectedItem, Mode=TwoWay}" />
I also wouldn't instantiate your value collection in the getter, but instead make the setter private, and assign a value to the property in your view model (e.g. the constructor or when the view model is activated).

PerformanceProgressBar "Invalid cross-thread access" exception

I am developing WP7 app. I met some unexpected behavior. I use PerformanceProgressBar from the SilverLight toolktit in my app in several pages. These PerformanceProgressBars are binded to ViewModel property called IsBusy. Each page has its own ViewModel.
....<toolkit:PerformanceProgressBar
VerticalAlignment="Top"
HorizontalAlignment="Left"
IsIndeterminate="{Binding IsBusy}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"
/>......
public bool IsBusy
{
get
{
return this._isBusy;
}
set
{
if (value == this._isBusy)
{
return;
}
this._isBusy = value;
RaisePropertyChanged("IsBusy");
}
}
When I change IsBusy value, I get "Invalid cross-thread access" exception.
Any ideas?
Any change to the visual-tree, i.e. the UI of your application, must be performed from the UI thread. This includes changes to properties that occur via bindings. My guess is that you are updating this property via a background-thread?
In this case, you need to marshal the property change onto the UI thread via the Dispatcher.
public bool IsBusy
{
get
{
return this._isBusy;
}
set
{
if (value == this._isBusy)
{
return;
}
Application.Current.Dispatcher.BeginInvoke(() => {
this._isBusy = value;
RaisePropertyChanged("IsBusy");
});
}
}
This exposes the view to your view-model, so is not very good MVVM! In this case I would 'hide' the dispatcher behind a single method interface IMarshalInvoke, that you provide to the ViewModel.
Or consider using BackgroundWorker which can fire ProgressChanged events onto the UI thread for you.