How do I bind a Picker async? - c#

I'm loading via httpClient some values into a List. Now I want to bind this List to a Picker. But the Picker is empty.
I have a class "Trade" with different items, e.g. title.
The ViewModel (FirmsViewModel) has the following code:
public async Task GetTradesData()
{
var tradeList = await App.RestService.GetTradesAsync(true);
Trades = new ObservableCollection<Trade>(tradeList);
}
The "Trades" List is filled. Till this point it seems to be working.
In my Page.cs file I have the following code:
public FirmsPage()
{
InitializeComponent();
viewModel = new FirmsViewModel();
BindingContext = viewModel;
}
protected async override void OnAppearing()
{
base.OnAppearing();
await viewModel.GetTradesData();
}
The XAML of the picker:
<Picker SelectedIndex="{Binding TradesSelectedIndex, Mode=TwoWay}"
ItemsSource="{Binding Trades}"
ItemDisplayBinding="{Binding title}"
Margin="0,15,0,0"
Title="Select a Trade">
</Picker>
If you are running the code, the Picker is always empty. Any suggestions?
Thanks.

That should be straight forward:
Make sure that you fire the PropertyChanged event after setting the Trades property
Make sure that this event is fired on the UI Thread
So if assuming your declaration of Trades looks like:
public ObservableCollection<Trade> Trades { get; private set; }
You could just call. RaisePropertyChanged("Trades"); (or whatever the equivalent is in your ViewModel type) right after assigning it in GetTradesData()
Alternatively you could change your declaration of your property:
private ObservableCollection<Trade> _trades;
public ObservableCollection<Trade> Trades
{
get => _trades;
set
{
_trades = value;
RaisePropertyChanged("Trades");
}
}
Or what I personally would prefer, is to simply initialize the ObservableCollection from the beginning and simply adding the items to it in GetTradesData():
public ObservableCollection<Trade> Trades { get; } = new ObservableCollection<Trade>();
and in GetTradesData():
foreach (var trade in tradeList)
Trades.Add(trade);

Related

ObservableCollection showing up in UI, but slow in Xamarin

There is a page where the user selects parameters to show the proper collection then on button click jumps to the next page (Coll) where it should show up.
User Selection Page XAML:
<ContentPage.BindingContext><xyz:UserSelectionViewModel</ContentPage.BindingContext>
...
<Button x:Name="Start" Command="{Binding LoadData}" Pressed="StartClick"/>
User Selection Page C#:
private async void ButtonClick(object sender, EventArgs e)
{
var vm = (CollViewModel)BindingContext;
vm.Hard = HardButtonSelected == Hard;
...
vm.Subject = vm.Subject.ToLower();
}
UserSelectionViewModel:
public class UserSelectionViewModel : BaseViewModel
{
public UserSelectionViewModel()
{
_dataStore = DependencyService.Get<IDataStore>();
_pageService = DependencyService.Get<IPageService>();
LoadData= new AsyncAwaitBestPractices.MVVM.AsyncCommand(FilterData);
FilteredData = new ObservableRangeCollection<Items>();
}
public async Task FilterData()
{
FilteredData.Clear();
var filtereddata = await _dataStore.SearchData(Hard, Subject).ConfigureAwait(false);
FilteredData.AddRange(filtereddata);
OnPropertyChanged("FilteredData");
Debug.WriteLine(FilteredData.Count());
await Device.InvokeOnMainThreadAsync(() => _pageService.PushAsync(new Coll(FilteredData)));
}
}
Coll XAML:
<ContentPage.BindingContext><xyz:CollViewModel</ContentPage.BindingContext>
...
<CarouselView ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type z:Coll}}, Path=InheritedData}" ItemTemplate="{StaticResource CollTemplateSelector}">
...
Coll C#:
public partial class Coll : ContentPage
{
public ObservableRangeCollection<Feladatok> InheritedData { get; set; }
public Coll(ObservableRangeCollection<Feladatok> x)
{
InitializeComponent();
InheritedData = x;
OnPropertyChanged(nameof(InheritedData));
}
}
CollViewModel:
public class CollViewModel : UserSelectionViewModel { ... }
BaseViewModel:
private ObservableRangeCollection<Feladatok> inheriteddata;
public ObservableRangeCollection<Feladatok> InheritedData
{
get
{
return inheriteddata;
}
set
{
if (value != inheriteddata)
{
inheriteddata = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("InheritedData"));
}
}
}
Managed to make it work like this with the help of Jason's tips. My only concern remaining is that: Won't this slow down the page that I load the observable collection two times basically? Is it a good practice as I have made it?
Eventually set the BindingContext to the VM and Binding from there. I still feel like it could be done more efficently or maybe that's how it is done. ViewModels are still new for me and I feel like it's much more code and slower with it. But I will close this, as it is working now.

WPF - DataGrid doesn't show list

My goal is to output a list in a datagrid, but this doesn't work and the datagrid is empty.
I tried to display the list in an other way and it did (but I can't remember what it was) and it worked, except for it not being in a datagrid but just data. I have changed up some things, but back then it reached the end and got displayed.
ViewModel in Mainwindow:
public class ViewModel
{
public List<ssearch> Items { get; set; }
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
//For simplicity, let's say this window opens right away
var Mdata = new MDataWindow { DataContext = DataContext };
Mdata.Show();
}
Other Window for data display:
string searchParam = "status = 1";
public MDataWindow()
{
InitializeComponent();
}
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items = Search(searchParam);
}
public List<ssearch> Search(string where)
{
{
//Lots of stuff going on here
}
return returnList;
}
And in WPF:
<Window x:Class="WPFClient.MDataWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFClient"
mc:Ignorable="d"
Title="MDataWindow" Height="Auto" Width="Auto">
<StackPanel>
<Button x:Name="AButton" Click="AButton_Click" Content="Load" />
<DataGrid ItemsSource="{Binding Items}" />
</StackPanel>
</Window>
I have no clue where the error is and tried to strip the code down as much as possible without killing error sources. The Datagrid just stays empty when I press the "Load" button.
EDIT:
I tried to convert the list into an observableColletion before passing it to the ViewModel, but this didn't work. I am working with a library, which I am not sure how to use observableCollection with, so I converted it instead of using it right away:
VM:
public ObservableCollection<Product> Items { get; set; }
Data Window:
List<Product> pp = Search_Products(searchParam);
var oc = new ObservableCollection<Product>(pp);
MainWindow.ViewModel.Instance.Items = oc;
First, change your List<Product> to an ObservableCollection<Product> as this will help to display the Items of the list on Add/Remove immediately.
This is because ObservableCollection implements the INotifyCollectionChanged interface to notify your target(DataGrid) to which it is bound, to update its UI.
Second, your binding can never work as expected due to changed reference of your collection.
private void AButton_Click(object sender, RoutedEventArgs e)
{
// You are changing your Items' reference completely here, the XAML binding
// in your View is still bound to the old reference, that is why you're seeing nothing.
//MainWindow.ViewModel.Instance.Items = Search(searchParam);
var searchResults = Search(searchParam);
foreach(var searchResult in searchResults)
{
MainWindow.ViewModel.Instance.Items.Add(searchResult);
}
}
Make sure you have changed the List to ObservableCollection upon running the Add loop, else you will get an exception saying the item collection state is inconsistent.
The ViewModel class should implement the INotifyPropertyChanged interface and raise its PropertyChanged event whenever Items is set to a new collection:
public class ViewModel : INotifyPropertyChanged
{
private List<ssearch> _items;
public List<ssearch> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(); }
}
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is required to notify the view regardless of the type of Items.
If you change the type of Items to ObservableCollection<T>, you should initialize the collection in the view model once:
public class ViewModel
{
public ObservableCollection<ssearch> Items { get; } = new ObservableCollection<ssearch>();
private static ViewModel _instance = new ViewModel();
public static ViewModel Instance { get { return _instance; } }
}
...and then add items to this collection instead of setting the property to a new one:
private void AButton_Click(object sender, RoutedEventArgs e)
{
MainWindow.ViewModel.Instance.Items.Clear();
var search = Search(searchParam);
if (search != null)
foreach (var x in search)
MainWindow.ViewModel.Instance.Items.Add(x);
}

How to bind whole page to BindingContext?

In my application I have a situation where I want to display some object on page and then change this object for different one.
So, let's consider I have MainPage.xaml.cs like this:
...
public Foo Item { get; set; }
public bool SomeCheck {
get {
return Item.Bar != "";
}
}
public MainPage() {
InitializeComponent();
SetItem();
BindingContext = this;
}
private void SetItem() {
Item = DifferentClass.GetNewItem();
}
private void Next_Clicked(object sender, EventArds e){
SetItem();
}
...
and MainPage.xaml like this:
...
<Label Text="{Binding Item.Bar}" IsVisible="{Binding SomeCheck}" />
<Button Text="Next" Clicked="Next_Clicked" />
...
So I want to bind whole page to BindingContext, to achieve this I've set BindingContext = this;. Behaviour which I want is to show Bar property of different objects returned by GetNewItem() and what I get is frozen page. In debugger Item is changing, but on page I have always value which I've set at the first call.
So the question is: can I somehow update BindingContext to show what I want? I tried calling OnPropertyChanged() but it doesn't work for me.
I know I can set up whole object like
BindingContext = { Bar = Item.Bar, SomeCheck = Item.Bar != "" };
and the it works, but of course my real scenario is more complex so I don't want to go this way.
Use OnPropertyChanged:
XAML:
<Label Text="IsVisible" IsVisible="{Binding MyIsVisible}" />
In the viewmodel, in your case in MainPage.xaml.cs:
private bool myIsVisible = true;
public bool MyIsVisible
{
get => myIsVisible;
set
{
myIsVisible = value;
OnPropertyChanged(nameof(MyIsVisible));
}
}

Set the SelectionChanged event of a ComboBox while binding its SelectedItem and ItemsSource in XAML

I'm trying to set up a ComboBox with its options binded from a list of strings, its default selected value binded from a setting, and with an event handler for its selection changed.
I want to configure it all using XAML like so:
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}"
SelectionChanged="RouteFilter_SelectionChanged" />
But when I do that on startup it throws the error:
An unhandled exception of type
'System.Reflection.TargetInvocationException' occurred in
PresentationFramework.dll
If I only do some of it in XAML, then either set the SelectionChanged event or the ItemsSource programatically in C# like below it works fine. But I have a lot of these ComboBoxes so I would rather do it straight in the XAML.
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}" />
With this C#:
public IEnumerable<string> Routes
{
get { return LubricationDatabase.GetRoutes(); }
}
public string DefaultRoute
{
get { return MySettings.Default.DefaultRoute; }
set { } /* side question: without this, it throws a parse exception. Any idea why? */
}
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
RoutesComboBox.SelectionChanged += RouteFilter_SelectionChanged;
}
I've also tried the solution found here:
private string _defaultRoute;
public string DefaultRoute
{
get { return MySettings.Default.DefaultRoute; }
set
{
if (_defaultRoute != value)
{
_defaultRoute = value;
// this fires before `SelectedValue` has been
// updated, and the handler function uses that,
// so I manually set it here.
RoutesComboBox.SelectedValue = value;
SelectionChangedHandler();
}
}
}
Which is okay, but is pretty bulky and probably more work than is worth it when I can just programatically assign the SelectionChanged event.
Again if possible I'd like to do it all using XAML because I have a lot of these ComboBoxes and initializing them all like this in the C# will look awful.
Any ideas?
Why are you binding with SelectedItem when you're not going to update the item when a user changes their selection? Not sure what your event handler is doing, but I have a working solution just the way you wanted it.
In short, you need to keep track of the DefaultRoute using a backing field. Also, you need to notify the UI when the selected item changes in your view model; which by the way is something you don't seem to be doing, MVVM. You should only be hooking into the selection changed event if you plan on updating the view in some way. All other changes should be handled in your view models DefaultRoute setter
XAML
<ComboBox Name="RoutesComboBox"
ItemsSource="{Binding Routes}"
SelectedItem="{Binding DefaultRoute}"
SelectionChanged="RouteFilter_SelectionChanged" />
Code
public partial class MainWindow : Window, INotifyPropertyChanged
{
public IEnumerable<string> Routes
{
get
{
return new string[] { "a", "b", "c", "d" };
}
}
public string DefaultRoute
{
get
{
return _defaultRoute;
}
set
{
_defaultRoute = value;
// Handle saving/storing setting here, when selection has changed
//MySettings.Default.DefaultRoute = value;
NotifyPropertyChanged();
}
}
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
DefaultRoute = MySettings.Default.DefaultRoute;
}
private string _defaultRoute;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void RouteFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
public static class MySettings
{
public static class Default
{
public static string DefaultRoute = "a";
}
}

Invalid cross-thread access with MVVM, ObservableCollection and Reactive Extensions (Rx)

I'm currently working on Windows Phone applications and I would like to use Reactive Extensions to create asynchronism to have a better UI experience.
I use the MVVM pattern: my View has a ListBox binded in my ViewModel to an ObservableCollection of Items. An Item has traditional properties like Name or IsSelected.
<ListBox SelectionMode="Multiple" ItemsSource="{Binding Checklist, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I instantiate my MainViewModel in App.xaml.cs:
public partial class App : Application
{
private static MainViewModel _viewModel = null;
public static MainViewModel ViewModel
{
get
{
if (_viewModel == null)
_viewModel = new MainViewModel();
return _viewModel;
}
}
[...]
}
And I load my data in MainPage_Loaded.
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
this.DataContext = App.ViewModel;
this.Loaded += new System.Windows.RoutedEventHandler(MainPage_Loaded);
}
private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
App.ViewModel.LoadData();
}
}
I load my data from my ViewModel (I'm going to move that code to the Model later):
public class MainViewModel : ViewModelBase
{
public ObservableCollection<Item> _checklist;
public ObservableCollection<Item> Checklist
{
get
{
return this._checklist;
}
set
{
if (this._checklist != value)
{
this._checklist = value;
}
}
}
private const string _connectionString = #"isostore:/ItemDB.sdf";
public void LoadData()
{
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
if (!context.DatabaseExists())
{
// Create database if it doesn't exist
context.CreateDatabase();
}
if (context.Items.Count() == 0)
{
[Read my data in a XML file for example]
// Save changes to the database
context.SubmitChanges();
}
var contextItems = from i in context.Items
select i;
foreach (Item it in contextItems)
{
this.Checklist.Add(it);
}
}
}
[...]
}
And it works fine, items are updated in the View.
Now I want to create asynchronism. With a traditional BeginInvoke in a new method that I call from the View instead of LoadData it works fine.
public partial class MainPage : PhoneApplicationPage
{
[...]
private void MainPage_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
App.ViewModel.GetData();
}
}
I use a property that I called CurrentDispatcher, it is filled in the App.xaml.cs with App.Current.RootVisual.Dispatcher.
public class MainViewModel : ViewModelBase
{
[...]
public Dispatcher CurrentDispatcher { get; set; }
public void GetData()
{
this.CurrentDispatcher.BeginInvoke(new Action(LoadData));
}
[...]
}
But I would like to use Reactive Extentions. So I tried different elements of Rx like ToAsync or ToObservable for example but I had some "UnauthorizedAccessException was unhandled" with "Invalid cross-thread access" when I add an item to the Checklist.
I tried to ObserveOn other threads cause maybe the error comes from mix between the UI and the background threads but it doesn't work. Maybe I don't use Rx like I would be in that particular case?
Any help would be much appreciate.
EDIT after your answers:
Here is a code which works great!
public void GetData()
{
Observable.Start(() => LoadData())
.ObserveOnDispatcher()
.Subscribe(list =>
{
foreach (Item it in list)
{
this.Checklist.Add(it);
}
});
}
public ObservableCollection<Item> LoadData()
{
var results = new ObservableCollection<Item>();
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
//Loading
var contextItems = from i in context.Items
select i;
foreach (Item it in contextItems)
{
results.Add(it);
}
}
return results;
}
As you see I didn't use correctly before. Now I can use a ObservableCollection and use it in the Susbscribe. It's perfect! Thanks a lot!
There appears to be no asynchronicity in your code (save the BeginInvoke, which I dont think is doing what you think it is doing).
I assume that you want to use Rx because your code is all blocking at the moment and the UI is unresponsive while the connection to the database is made and the data is loaded :-(
What you want to do is perform the "heavy lifting" on a background thread and then once you have the values just add them to the ObservableCollection on the Dispatcher. You can do this one of three ways:
Return the entire collection in one go. You then loop through the list with a foreach loop adding to the ObservableCollection. This has the potential downside of blocking the UI (unresponsive app) if the list is too large
Return the collection one value at a time, adding each item to the ObservableCollection in an independent call to the dispatcher. This will keep the UI responsive but can take much longer to complete
Return the collection in buffered chunks and try to get the best of both worlds
The code you want may look like this
public IList<Item> FetchData()
{
using (ItemDataContext context = new ItemDataContext(_connectionString))
{
//....
var results = new List<Item>();
foreach (Item it in contextItems)
{
results.Add(it);
}
return results;
}
}
public void LoadData()
{
Observable.Start(()=>FetchData())
.ObserveOnDispatcher()
.Subscribe(list=>
{
foreach (Item it in contextItems)
{
this.Checklist.Add(it);
}
});
}
The code ain't ideal for Unit testing, but it appears that this is not of interest to you any way (static members, VM with DB connectsions etc..) so this might just work?!
I just ran into this myself. There are multiple ways to get to the dispatcher, I had to use the following. I call this directly from my ViewModels (no passing in from outside).
Application.Current.Dispatcher.Invoke(action);

Categories