My question is similar to some others that I have read, but I have not been able to find an answer to my specific issue.
Note:
I have read the following questions before asking:
ListView Data Binding for Windows 8.1 Store Apps
WinRT ViewModel DataBind to async method
That being said, I am creating a Windows 8.1 application that loads a text file asynchronously, and binds the data to a ListBox. I am sure that the issue has something to do with non-UI threads are not able to update the UI, so even though my data source implements INotifyPropertyChanged, the UI is not being updated when the data is loaded. Here is my LoadPayees() method:
public async void LoadPayees()
{
try
{
var json = await FileService.ReadFromFile("folder", "payees.json");
IList<Payee> payeesFromJson = JsonConvert.DeserializeObject<List<Payee>>(json);
var payees = new ObservableCollection<Payee>(payeesFromJson);
_payees = payees;
}
catch (Exception)
{
throw;
}
if (_payees == null)
{
_payees = new ObservableCollection<Payee>();
}
}
LoadPayees() is called in the OnNavigatedTo() event of my page. I can see via breakpoints that the method is being called, and the payees are being loaded into an ObservableCollection<Payee>. _payees is a property, which calls OnPropertyChanged() when it is set.
My question is, is there a way to have the UI thread be updated after LoadPayees() is finished loading the data? I also read somewhere that using a Task is no good for the UI as well. My static method FileService.ReadFromFile() returns a Task<string>.
Edit:
Here is my method ReadFromFile() which also calls to OpenFile():
public static async Task<string> ReadFromFile(string subFolderName, string fileName)
{
SetupFolder();
var file = await OpenFile(subFolderName, fileName);
var fileContents = string.Empty;
if (file != null)
{
fileContents = await FileIO.ReadTextAsync(file);
}
return fileContents;
}
public static async Task<StorageFile> OpenFile(string subFolderName, string fileName)
{
if (_currentFolder != null)
{
var folder = await _currentFolder.CreateFolderAsync(subFolderName, CreationCollisionOption.OpenIfExists);
return await folder.GetFileAsync(fileName);
}
else
{
return null;
}
}
Edit 2:
Here is the code for the properties, View, and OnNavigatedTo() as requested.
-- properties of the ViewModel --
private ObservableCollection<Payee> _payees;
private Payee _currentPayee;
public PayeesViewModel()
{
_currentPayee = new Payee();
_payees = new ObservableCollection<Payee>();
}
public ObservableCollection<Payee> Payees
{
get { return _payees; }
set
{
_payees = value;
OnPropertyChanged();
}
}
public Payee CurrentPayee
{
get { return _currentPayee; }
set
{
_currentPayee = value;
OnPropertyChanged();
}
}
-- view --
<StackPanel Orientation="Horizontal"
DataContext="{Binding Path=CurrentPayee}"
Grid.Row="1">
<Grid>
<!-- snip unrelated Grid code -->
</Grid>
<ListBox x:Name="PayeesListBox"
Margin="50,0,50,0"
Width="300"
ItemsSource="{Binding Path=Payees}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=CompanyName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
-- code behind --
private PayeesViewModel _vm = new PayeesViewModel();
public PayeesPage()
{
this.InitializeComponent();
this._navigationHelper = new NavigationHelper(this);
this._navigationHelper.LoadState += navigationHelper_LoadState;
this._navigationHelper.SaveState += navigationHelper_SaveState;
DataContext = _vm;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
_navigationHelper.OnNavigatedTo(e);
_vm.LoadPayees();
}
I think the problem is that you set the DataContext twice.
<StackPanel Orientation="Horizontal"
DataContext="{Binding Path=CurrentPayee}"
Grid.Row="1">
and
DataContext = _vm;
ListBox is a child from the outer StackPanel with DataContext CurrentPayee. On CurrentPayee you don't have Payees. You should not set DataContext multiple times.
Btw. change your code like following:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
_navigationHelper.OnNavigatedTo(e);
await _vm.LoadPayees();
}
public async Task LoadPayees()
{
try
{
var json = await FileService.ReadFromFile("folder", "payees.json");
IList<Payee> payeesFromJson = JsonConvert.DeserializeObject<List<Payee>>(json);
var payees = new ObservableCollection<Payee>(payeesFromJson);
_payees = payees;
}
catch (Exception)
{
throw;
}
if (_payees == null)
{
_payees = new ObservableCollection<Payee>();
}
}
You should never write async void for methods except event handlers.
Edit:
Change the ObservableCollection in your ViewModel. You should not have a public setter for a list.
private readonly ObservableCollection<Payee> _payees = new ObservableCollection<Payee>();
public ObservableCollection<Payee> Payees
{
get { return _payees; }
}
Than loop and add the items to the collection. Now, the view is notified.
foreach (var item in payeesFromJson)
{
Payees.Add(item);
}
Related
Need help regarding CollectionView not loading data from API upon loading. I need to pull-down refresh first before the data will show. I'm doing it MVVM.
Below is my XAML:
<RefreshView x:DataType="local:MainPageViewModel" Command="{Binding LoadReleaseDocuments}"
IsRefreshing="{Binding IsRefreshing ,Mode=OneWay}"
RefreshColor="#FFFF7F50">
<CollectionView x:Name="SuccessList"
ItemsSource="{Binding ReleasedDocuments}"
SelectionMode="Single" >
...
</CollectionView>
</RefreshView>
And this is my code behind:
public ObservableCollection<Release> ReleasedDocuments { get; }
public MainPageViewModel()
{
// collection
ReleasedDocuments = new ObservableCollection<Release>();
DeliveredDocuments = new ObservableCollection<Deliver>();
CallNow = new Command<Deliver>(Call_Now);
//Load
LoadDocuments = new Command(ExecuteLoadItemsCommand);
LoadReleaseDocuments = new Command(ExecuteCommand);
}
And below code is where I get my data thru API calls
void ExecuteCommand()
{
bool forceRefresh = true;
if (IsRefreshing)
return;
IsRefreshing = true;
Device.BeginInvokeOnMainThread(async () =>
{
try
{
ReleasedDocuments.Clear();
switch (Application.Current.Properties["Position"])
{
case string a when a.Contains("Courier"):
var items = await DataStore.GetItemsAsync(forceRefresh, Application.Current.Properties["Position"].ToString(), "tmdm");
items = items.Where(ab => ab.TMNo != null).Where(ac => ac.TMNo.Contains("DM"));
var sortedItems = items.OrderByDescending(c => c.OrderDate);
CourierDMData(sortedItems);
break;
}
}
catch (Exception ex)
{
IsBusy = false;
IsRefreshing = false;
...
}
finally
{
IsBusy = false;
IsRefreshing = false;
}
});
IsRefreshing = false;
}
And inserting it to ObservableCollection
void CourierDMData(IOrderedEnumerable<Summary> sortedItems)
{
ReleasedDocuments.Clear();
foreach (var itemx in sortedItems)
{
if (itemx.statusId == 0)
{
ReleasedDocuments.Add(new Release()
{
Id = itemx.Id,
....
});
}
}
CountRelease = ReleasedDocuments.Count;
}
When debugging, I can get the CountRelease = ReleasedDocuments.Count; value (count) it is displaying correctly the value, but the CollectionView is not showing anything until I refresh.
I'm usually doing a work around and call refresh with the PageAppearingEvent and use Xamarin Community Toolkit EventToCommandBehavior to call a function which calls the refresh function with a small delay if necessary. This way I don't have to manually refresh each time I open the page.
XAML example:
<ContentPage.Behaviors>
<xct:EventToCommandBehavior
EventName="Appearing"
Command="{Binding AppearingCommand}"/>
</ContentPage.Behaviors>
MVVM example:
public MyViewModel() //constructor
{
AppearingCommand = new Command(OnAppearing);
}
public ICommand AppearingCommand { get; }
private void OnAppearing()
{
await System.Threading.Tasks.Task.Delay(int delay); //only if necessary because of initialization
Refresh(); //Or else set your public properties
}
I have an error "Must create DependencySource on same Thread as the DependencyObject" in my project.
My comment is used to load a file and create a list. This list is bind to a ListBox. AL was working good. But I created a Task to load (load can be long). Now I have this error. I don't understand why it occurs.
There is my code :
MainView.xaml:
<ListBox ItemsSource="{Binding Results}"
SelectedItem="{Binding SelectedItem}">
<ListBox.InputBindings>
<KeyBinding Command="{Binding RemoveCommand}"
Key="Delete"/>
</ListBox.InputBindings>
</ListBox>
<Button Grid.Row="1" Grid.Column="0"
Style="{StaticResource StyleButton}"
Command="{Binding LoadCommand}"
Content="Open result"/>
MainViewModel:
#region Fields/Properties
public ImageWithPoints SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
SelectedPointIndex = 1;
OnPropertyChanged();
OnPropertyChanged("Picture");
UpdatePoints();
}
}
public List<ImageWithPoints> Results
{
get
{
return _results;
}
set
{
_results = value;
if (value == null)
{
SelectedPointIndex = 0;
}
OnPropertyChanged();
}
}
public BitmapSource Picture
{
get
{
return SelectedItem?.Picture;
}
}
#endregion
#region Load
private ICommand _loadCommand;
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(OnLoad, CanLoad);
return _loadCommand;
}
}
public void OnLoad()
{
StartRunning(this, null);
Task loadTask = new Task(new Action(() =>
{
Load();
Application.Current.Dispatcher.Invoke(new Action(() =>
{
StopRunning(this, null);
}));
}));
loadTask.Start();
}
public bool CanLoad()
{
return !IsRunning;
}
#endregion
#region Events
public event EventHandler OnStartRunning;
public event EventHandler OnStopRunning;
private void StartRunning(object sender, EventArgs e)
{
OnStartRunning(sender, e);
}
private void StopRunning(object sender, EventArgs e)
{
OnStopRunning(sender, e);
}
#enregion
#region Methods
public void Load()
{
// Open File
// Set to list
List<ImageWithPoints> listRes;
Results = listRes;
SelectedItem = Results[0];
}
#endregion
When I remove the line SelectedItem = Results[0]; I have no error (but application don't work has it should).
Set the SelectedItem property back on the UI thread once the Task has finished:
public void OnLoad()
{
StartRunning(this, null);
Task.Factory.StartNew(new Action(() =>
{
Load();
})).ContinueWith(task =>
{
SelectedItem = Results[0];
StopRunning(this, null);
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
You can only access a UI element on the thread on which it was originally created so if your UpdatePoints() method accesses any control you must call this method on the UI thread.
I have a ViewModel like this:
public class WelcomeWindowVm : ViewModel
{
private ViewModel view;
public WelcomeWindowVm(){
this.View = new LoginVm() {
Completed += (o, e) => {
this.View = new OtherVm(e.User){
Completed += (o, e) =>; // and so on
}
}
};
}
public ViewModel View {
get {
return this.view;
}
set {
this.view = value;
this.OnPropertyChanged(nameof(this.View));
}
}
}
LoginVm is another Viewmodel whose Completed event is triggered when a Command on it is completed (The event is only triggered when correct login credentials are used). OtherVm is another vm whose completed event is triggered for whatever reason.
I render the View using a DataTemplate. For example:
<Window.Resources>
<DataTemplate DataType="vm:LoginVm">
Textboes and buttons here
</DataTemplate>
<DataTemplate DataType="vm:OtherVm">
...
</DataTemplate>
</Window.Resources>
<ContentControl Content={Binding View} />
The DataContext of this window is set to WelcomeWindowVm class above, before ShowDialog.
This works well. When the Window is shown using ShowDialog, LoginVm is shown. Then OtherVm when whatever task of LoginVm is completed, and so on.
Now I thought of converting the Completion stuff to Async/await pattern. The LoginVm now looks like this:
public LoginVm{
...
private TaskCompletionSource<User> taskCompletionSource = new TaskCompletionSource<User>();
...
// This is the Relay command handler
public async void Login()
{
// Code to check if credentials are correct
this.taskCompletionSource.SetResult(this.user);
// ...
}
public Task<User> Completion(){
return this.taskCompletionSource.Task;
}
}
Instead of this:
public LoginVm{
public event EventHandler<CustomArgs> Completed;
// This is the Relay command handler
public async void Login()
{
// Code to check if credentials are correct
OnCompleted(this.user);
// ...
}
}
So that I can use it like this:
public WelcomeWindowVm(){
var loginVm = new LoginVm();
this.View = new LoginVm();
User user = await loginVm.Completion();
var otherVm = new OtherVm(user);
this.View = otherVm;
Whatever wev = await otherVm.Completion();
//And so on
}
But I can't use await in a Constructor and even if I use an async Method for that, how will I call it in another class after calling ShowDialog since ShowDialog blocks?
I think using an async void will work. But from what I have heard, it should be avoided unless I am using it in an event handler.
Maybe use an async Task method but not await it?
You can do it like this:
public WelcomeWindowVm() {
var loginVm = new LoginVm();
this.View = loginVm;
loginVm.Completion().ContinueWith(loginCompleted =>
{
var otherVm = new OtherVm(loginCompleted.Result);
this.View = otherVm;
otherVm.Completion().ContinueWith(whateverCompleted =>
{
});
});
}
I developed an application on Windows 10 Universal App who use MVVM but I have a big problem with it.
I would add an ObservableCollection item(created on a second window) to the MVVM and then, show the new item on the ListView of MainPage but it doesn't refresh!
The 2 windows are always open
http://i.stack.imgur.com/WSo6v.jpg
The code of MVVMList.cs
public class MVVMList : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<THEFile> onglets_cache = new ObservableCollection<THEFile>();
public ObservableCollection<THEFile> onglets_list
{
get
{
return onglets_cache;
}
set
{
onglets_cache = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
public MVVMList()
{
onglets_list = new ObservableCollection<THEFile>();
Fonctions fonctions = new Fonctions();
fonctions.LoadOnglets(onglets_cache);
}
}
The code of the second page(always open) - CreateFile.xaml.cs
private void create_butt_Click(object sender, RoutedEventArgs e)
{
Fonctions fonc = new Fonctions(); MVVMList main = new MVVMList();
fonc.SetupNew(main.onglets_list, "test" + ".php", "");
}
//SetupNew on Fonctions.cs
public async void SetupNew(ObservableCollection<THEFile> list, string name, string content)
{
FolderPicker folderpick = new FolderPicker();
folderpick.ViewMode = PickerViewMode.List;
folderpick.FileTypeFilter.Add(".html"); folderpick.FileTypeFilter.Add(".htm"); folderpick.FileTypeFilter.Add(".HTML");
folderpick.FileTypeFilter.Add(".php"); folderpick.FileTypeFilter.Add(".PHP");
folderpick.FileTypeFilter.Add(".css"); folderpick.FileTypeFilter.Add(".CSS");
folderpick.FileTypeFilter.Add(".js"); folderpick.FileTypeFilter.Add(".JS");
StorageFolder storage_file = await folderpick.PickSingleFolderAsync();
if (storage_file != null)
{
MainPage vm = new MainPage();
list.Add(new THEFile { NameOfFile = name, PathOfFile = storage_file.Path + "\\" + name, CodeOfFile = content, already_opened = false, line = 0 });
string path = storage_file.Path + #"\" + name;
StorageFile file_create = await storage_file.CreateFileAsync(name, CreationCollisionOption.GenerateUniqueName);
Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(file_create);
SaveOnglets(list);
}
}
And on the MainPage.xaml (always open)
...
<ListView x:Name="onglets" x:FieldModifier="public" ItemTemplate="{StaticResource Templa}" ItemsSource="{Binding onglets_list}" SelectionChanged="onglets_SelectionChanged" Margin="0,117,0,57" Visibility="Visible" ContainerContentChanging="onglets_ContainerContentChanging">
...
Thank you!
In your XAML, try using a Collection View Source.
Add this to the top of your xaml:
<Page.Resources>
<CollectionViewSource x:Name="MakesCollectionViewSource" IsSourceGrouped="True"/>
</Page.Resources>
Set your ListView:
ItemsSource="{Binding Source={StaticResource MakesCollectionViewSource}}"
Then in your code when you have a List of items assign it using
MakesCollectionViewSource.Source = /* Some List<GroupInfoList<object>> that is generated from onglets_list*/
I create my List like this but it may not be relevant because this is to make all of my object names alphabetical:
internal List<GroupInfoList<object>> GetGroupsByLetter()
{
var groups = new List<GroupInfoList<object>>();
var query = from item in MakeList
orderby ((Make)item).MakeName
group item by ((Make)item).MakeName[0] into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
var info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
public class GroupInfoList<T> : List<object>
{
public object Key { get; set; }
public new IEnumerator<object> GetEnumerator()
{
return (System.Collections.Generic.IEnumerator<object>)base.GetEnumerator();
}
}
where MakeList is my observable collection and Make are the objects in the collection and MakeName is a string I am trying to alphabetize
And then call using
MakesCollectionViewSource.Source = GetGroupsByLetter();
If I understand your code and requirements correctly, I think part of the problem is that you "new up" your MVVMList and your MainPage everytime you click the create button.
So, without getting into suggestions about using MVVM Light and an IOC container, you could quickly accomplish what you're trying to do by making your MVVMList class a singleton and having your MainPage use it for a data context. When your other window adds to the MVVMList.onglets collection, it will be immediately reflected in your currently open MainPage. Let me know if you need some code snippets. Good luck!
[Edit below]
I had a few minutes left on lunch, so here is an over-simplified example. Again, without getting into what MVVM is and is not. Personally, I would do this differently, but that would be outside the scope of your question. Full disclosure - this is in WPF, but same logic applies, I just don't have Windows 10 on the PC that I'm using. I also simplified the collection to be of type string. This is not intended to copy/paste into your code as it will not work in your example - but should easily transfer.
MVVMList class:
public class MVVMList: INotifyPropertyChanged
{
//Singleton section
private static MVVMList instance;
private MVVMList() { }
public static MVVMList Instance
{
get
{
if (instance == null)
{
instance = new MVVMList();
}
return instance;
}
}
//end singleton section
private ObservableCollection<string> _onglets = new ObservableCollection<string>();
public ObservableCollection<string> Onglets
{
get { return _onglets; }
set
{
if (_onglets != value)
{
_onglets = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
}
//INotify implementation
public event PropertyChangedEventHandler PropertyChanged;
}
MainPage:
<ListView x:Name="onglets" x:FieldModifier="public" ItemsSource="{Binding Onglets}" />
MainPage.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = MVVMList.Instance;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var x = new CreateWindow();
x.Show();
}
}
CreateWindow.cs:
private void CreateButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(StringTextBox.Text))
{
MVVMList.Instance.Onglets.Add(StringTextBox.Text);
}
}
I have a Windows Store app (or Metro style / WinRT / Windows Runtime) that has a CollectionViewSource that is bound to a ListView in the codebehind file. Without sorting and/or grouping all works fine and the UI gets updated. But as soon as I sort or group the CollectionViewSource's Source, the UI stops updating. Set the binding in MainPage:
public MainPage()
{
InitializeComponent();
ViewModel = new MainPageVm();
DataContext = ViewModel;
Binding myBinding = new Binding();
myBinding.Mode = BindingMode.TwoWay;
myBinding.Source = ViewModel.UpcomingAppointments;
UpcomingListView.SetBinding(ListView.ItemsSourceProperty, myBinding);
//Timer to update the UI periodically
var Timer = new DispatcherTimer {Interval = TimeSpan.FromSeconds(60)};
Timer.Tick += (o, e) => ViewModel.LoadData();
Timer.Start();
}
The relevant part of the viewmodel:
public class MainPageVm : INotifyPropertyChanged
{
public MainPageVm()
{
Appointments = new ObservableCollection<Appointment>();
Appointments.CollectionChanged += delegate { NotifyPropertyChanged("UpcomingAppointments"); };
}
public ObservableCollection<Appointment> Appointments
{
get { return appointments; }
set
{
if (appointments != value)
{
appointments = value;
NotifyPropertyChanged();
NotifyPropertyChanged("UpcomingAppointments");
}
}
}
public CollectionViewSource UpcomingAppointments
{
get
{
return new CollectionViewSource
{
//removing Where an Groupby and set IsSourceGrouped = false get the UI updated again
Source = Appointments
.Where(a => a.Start > DateTime.UtcNow)
.GroupBy(x => x.Day),
IsSourceGrouped = true
};
}
}
public async Task LoadData()
{
var Repo = new WebserviceRepository();
await Repo.GetAppointments();
Appointments.Clear;
foreach (Appointment Appointment in Repo.Appointments)
{
Appointments.Add(Appointment);
}
NotifyPropertyChanged("UpcomingAppointments");
NotifyPropertyChanged("Appointments");
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
And the relevant XAML part:
<ListView Name="UpcomingListView" ItemTemplate="{StaticResource AppointmentListTemplate}">
<ListView.GroupStyle>
<GroupStyle HidesIfEmpty="False">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Key}" />
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
And finally the Appointment class:
public class Appointment
{
public Guid AppointmentId { get; set; }
public string Description { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
public string Day
{
get
{
if (this.Start.Date == DateTime.Now.Date){return "Today";}
if (this.Start.Date == (DateTime.Now.AddDays(1).Date)) { return "Tomorrow";}
If (this.Start.Date == (DateTime.Now.AddDays(2).Date)){ return "Day After Tomorrow";}
return string.Empty;
}
}
}
So deleting the .Where and .Sort and set IsSourceGrouped to false makes the UI update again, but of course unsorted and not the desired grouping.
When I set data in the constructor of the ViewModel, that data is displayed (grouped or not) but not updated
As you can see I call NotifyPropertyChanged whenever LoadData is run, just to be sure that it is called. I also run NotifyPropertyChanged after the Appointments OberservableCollection-s Collection has changed.
Apparently the Where filtering and the GroupBy grouping needs some other tweaks that I am not aware of. I see a lot of question on SO about grouping, but it seems that they are about WPF (and that is different) or set all the bindings and static resources in XAML (and I don't want to do that).
So my basic question is: How can I group and sort the UpcomingAppointments in a ListView using the C# codebehind file?