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 =>
{
});
});
}
Related
I am trying to call this GetProductStatus() method on a page button click event, but it's loading before the button click. Means when the ViewModel is loading, this is also load automatically.
I would like to declared this VM method "GetProductStatus()" to be called only when a button click event occurs.
ViewModel method:
private async void GetProductStatus()
{
try
{
IsBusy = true;
var status = await ProductStatusService.GetProductStatus(new ProductStatus()
{
StoreCode = s_code,
StartTime = StartDateValue.AddMinutes(time1),
EndTime = StartDateValue.AddMinutes(time2)
});
IsBusy = false;
if (status != null)
{
//Process happens
}
else
{
//Array is Null
}
ProductStatus = status;
}
catch (Exception)
{
ProductStatus = null;
}
}
Here, the method is declared.
public ProductViewModel(INavigation nav, Store store)
{
_Nav = nav;
GetProductStatus();
}
Here, the clicked event.
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
I would like to declared this VM method "GetProductStatus()" to be
called only when a button click event occurs.
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
For above code you posted, we can find that the constructor of your viewmodel will be called as soon as you call code new ProductPage(_ViewModel).
So, you can try to remove code GetProductStatus(); in constructor ProductViewModel
public ProductViewModel(INavigation nav, Store store)
{
_Nav = nav;
// remove code here
//GetProductStatus();
}
and add a command in your ViewModel, and bind it to the button in your page.
Please refer to the following code:
public class ProductViewModel
{
public Command LoadDataCommand { get; set; }
public ProductViewModel() {
LoadDataCommand = new Command(loadData);
// remove code here
//GetProductStatus();
}
private void loadData()
{
GetProductStatus(); // add your code here
}
private async void GetProductStatus()
{
// other code
}
}
Note:
1.In this condition, you can also navigate as follows:
private async void ProductTypeButton_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new ProductPage(_ViewModel));
}
2.I don't add parameter to the constructor of ProductViewModel , you can modify above code I posted according to your needs.
Set aside the fact that you are working with views and models. Simply think of them like any other class in c#.
If you need to tell class A "do something under these circumstances`, what are your options?
Pass a parameter in constructor: public ProductViewModel(..., bool doGetProductStatus)..., usage: new ProductViewModel(..., true);
Call a method A.DoSomething(); after you've created it: _ViewModel.DoSomething();
Use MessagingCenter Publish/Subscribe.
In my app, I want to know in the View when Initialize is complete. The problem is that it gets launched before you can hook InitializeTask.PropertyChanged. Here is my ViewModel code:
public override async Task Initialize()
{
ClientID = await MyDataSource.GetClientID();
}
In my View I am doing the following:
protected override void OnViewModelSet()
{
var vm = this.DataContext as MyViewModel;
vm.InitializeTask.PropertyChanged += InitializeTask_PropertyChanged;
base.OnViewModelSet();
}
private void InitializeTask_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSuccessfullyCompleted")
{
vm.InitializeTask.PropertyChanged -= InitializeTask_PropertyChanged;
if (vm.ClientID != "")
posClient = new PosClient(this, vm.ClientID);
}
}
As far as I know, OnViewModelSet is the soonest that you have access to your ViewModel in your View. If I put in breakpoints, Initialize runs before OnViewModelSet is fired. This makes it very likely that Initialize has finished before you can hook the event. Is there another place in the View where you can hook the event before Initialize starts so you will be guaranteed to Initialize.PropertyChanged will fire in the View?
**** Update ****
I followed #fmaccaroni advice and implemented an MvxInteraction called DataLoaded. One thing I did different was to create a separate function to load the data.
public void LoadData()
{
Task.Run(async () =>
{
ClientID = await IHSDataSource.GetClientID();
_DataLoaded.Raise();
});
}
I was concerned about the async task finishing before I got the interaction event wired up. Doing it this way, I added this in the View.
protected override void OnViewModelSet()
{
vm = this.DataContext as InvoiceViewModel;
var set = this.CreateBindingSet<InvoiceView, InvoiceViewModel>();
set.Bind(this).For(view => view.DataLoaded).To(viewModel => viewModel.DataLoaded).OneWay();
set.Apply();
vm.LoadData();
base.OnViewModelSet();
}
This way, LoadData does not start until I am sure the result will trigger the interaction and I am guaranteed to get the result. This was the first I had heard about MvxInteraction and I am now using it all the time.
I'm not sure what you want to achieve, but if you want to take an action in your view when the Initialize ends just do an MvxInteraction and call it after your await, i.e.:
ViewModel:
private MvxInteraction _interaction = new MvxInteraction();
public IMvxInteraction MyMvxInteraction => _interaction;
public override async Task Initialize()
{
ClientID = await MyDataSource.GetClientID();
this._interaction.Raise();
}
View:
private IMvxInteraction _interaction;
public IMvxInteraction MyMvxInteraction
{
get => this._interaction;
set
{
if (this._interaction != null)
this._interaction.Requested -= this.OnInteractionRequested;
this._interaction = value;
this._interaction.Requested += this.OnInteractionRequested;
}
}
private void OnInteractionRequested(object sender, EventArgs e)
{
var vm = this.DataContext as MyViewModel;
if (vm.ClientID != "")
posClient = new PosClient(this, vm.ClientID);
}
and the binding in the view:
var set = this.CreateBindingSet<MyView, MyViewModel>();
set.Bind(this).For(view => view.MyMvxInteraction).To(viewModel => viewModel.MyMvxInteraction).OneWay();
set.Apply();
I have a page and on clicking a plus button on toolbar i am calling a popup page
from popup page user can add a new entry or cancel / close window without doing anything
Everything is working fine and code is like this
public partial class SelectSchool : ContentPage
{
public SelectSchool()
{
InitializeComponent();
#region toolbar
ToolbarItem tbi = null;
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
Navigation.PushModalAsync(target_page);
}, 0,0);
}
ToolbarItems.Add(tbi);
#endregion
this.Title = "Select School";
}
}
And my popup page is like
public partial class AddSchool : ContentPage
{
public AddSchool()
{
InitializeComponent();
}
private async void Button_OK_Clicked(object sender, EventArgs e)
{
//doing some operations like entry to db etc and close page
Navigation.PopModalAsync();
}
private void cancelClicked(object sender, EventArgs e)
{
Navigation.PopModalAsync();
}
}
But now i want to wait for the Popup to get closed to do some additional coding and i tried below code
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
await Navigation.PushModalAsync(target_page);
//await till target_page is closed and once its closed call my next function here
}, 0,0);
}
But await is not working . How can i await on this area till the popup getting closed ? Any idea??
Use the Disappearing event on your modal page.
Example:
var modalPage = new ContentPage();
modalPage.Disappearing += (sender2, e2) =>
{
System.Diagnostics.Debug.WriteLine("The modal page is dismissed, do something now");
};
await content.Navigation.PushModalAsync(modalPage);
System.Diagnostics.Debug.WriteLine("The modal page is now on screen, hit back button");
Or use a EventWaitHandle:
var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
var modalPage = new ContentPage();
modalPage.Disappearing += (sender2, e2) =>
{
waitHandle.Set();
};
await content.Navigation.PushModalAsync(modalPage);
System.Diagnostics.Debug.WriteLine("The modal page is now on screen, hit back button");
await Task.Run(() => waitHandle.WaitOne());
System.Diagnostics.Debug.WriteLine("The modal page is dismissed, do something now");
A bit late on the answer here, but it might be best to listen to the Application's OnModalPagePopping event handler.
First, create the modal page. Give it a property to store the data you want to later retrieve:
public class MyModalPage : ContentPage
{
public string Data { get; set; }
public MyModalPage()
{
InitializeComponent();
// ... set up the page ...
}
private async void PopThisPage()
{
// When you want to pop the page, just call this method
// Perhaps you have a text view with x:Name="PhoneNumber", for example
Data = PhoneNumber.Text; // store the "return value" before popping
await MyProject.App.Current.MainPage.Navigation.PopModalAsync();
}
}
In the parent page, you can create the modal page, and set up the event handler to listen for when the modal page pops:
public class MyPage : ContentPage
{
MyModalPage _myModalPage;
public MyPage()
{
InitializeComponent();
// ... set up the page ...
}
private async void ShowModalPage()
{
// When you want to show the modal page, just call this method
// add the event handler for to listen for the modal popping event:
MyProject.App.Current.ModalPopping += HandleModalPopping;
_myModalPage = new MyModalPage();
await MyProject.App.Current.MainPage.Navigation.PushModalAsync(_myModalPage());
}
private void HandleModalPopping(object sender, ModalPoppingEventArgs e)
{
if (e.Modal == _myModalPage)
{
// now we can retrieve that phone number:
var phoneNumber = _myModalPage.Data;
_myModalPage = null;
// remember to remove the event handler:
MyProject.App.Current.ModalPopping -= HandleModalPopping;
}
}
}
This is better than using the OnDisappearing method, as others have already stated that can be called when the app is backgrounded, etc. And its behavior is not consistent across platforms.
There is also another event OnModalPopped, which is called after the modal is completely popped from the navigation stack. If using that, it should work similarly.
You can try to create an event, call when pop close.
public partial class AddSchool : ContentPage
{
public delegate void PopupClosedDelegate();
public event PopupClosedDelegate PopupClosed;
public AddSchool()
{
InitializeComponent();
}
private async void Button_OK_Clicked(object sender, EventArgs e)
{
//doing some operations like entry to db etc and close page
await Navigation.PopModalAsync();
if (PopupClosed!=null)
{
PopupClosed();
}
}
private async void cancelClicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
if (PopupClosed != null)
{
PopupClosed();
}
}
}
I put it on the button click event, maybe you can put on close or dispose event. Then here is implement
public partial class SelectSchool : ContentPage
{
public SelectSchool()
{
InitializeComponent();
#region toolbar
ToolbarItem tbi = null;
if (Device.OS == TargetPlatform.Android)
{
tbi = new ToolbarItem("+", "plus", async () =>
{
var target_page = new AddSchool();
target_page.PopupClosed += () => { /*Do something here*/ };
Navigation.PushModalAsync(target_page);
}, 0, 0);
}
ToolbarItems.Add(tbi);
#endregion
this.Title = "Select School";
}
}
Hope this help.
I create an extension method for this:
public static class DialogUtils
{
public static async Task ShowPageAsDialog(this INavigation navigation, Page page)
{
int pagesOnStack = navigation.NavigationStack.Count + 1;
var waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
page.Disappearing += (s, e) =>
{
if (navigation.NavigationStack.Count <= pagesOnStack)
waitHandle.Set();
};
await navigation.PushAsync(page);
await Task.Run(() => waitHandle.WaitOne());
}
}
And I can use it:
private async void bShowDialogPage_Clicked(object sender, EventArgs e)
{
var page = new DialogPage();
await page.LoadData();
await Navigation.ShowPageAsDialog(page);
var result = page.PageResult;
}
It supports situations when dialog page show another page. I prefer NavigationStack instead ModalStack due to NavigationPage and BackButton.
Another way of accomplishing this is by calling an event from the page's OnDisapearing method, this event can then be subscribed by the navigation service which you create, and you can then use the "TaskCompletionSource" to wati until your page finishes its work and then complete the task.
For more details about accomplishing this, you can check this blog post.
Here is the base page's implementation, every page in this demo app inherit this page:
public class BasePage<T> : ContentPage
{
public event Action<T> PageDisapearing;
protected T _navigationResut;
public BasePage()
{
}
protected override void OnDisappearing()
{
PageDisapearing?.Invoke(_navigationResut);
if (PageDisapearing != null)
{
foreach (var #delegate in PageDisapearing.GetInvocationList())
{
PageDisapearing -= #delegate as Action<T>;
}
}
base.OnDisappearing();
}
}
Here is an overview of the navigation service you should use:
public async Task<T> NavigateToModal<T>(string modalName)
{
var source = new TaskCompletionSource<T>();
if (modalName == nameof(NewItemPage))
{
var page = new NewItemPage();
page.PageDisapearing += (result) =>
{
var res = (T)Convert.ChangeType(result, typeof(T));
source.SetResult(res);
};
await App.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page));
}
return await source.Task;
}
To call this page with the navigation service, you can use the following code:
var item = await new SimpleNavigationService().NavigateToModal<Item>(nameof(NewItemPage));
Items.Add(item);
Scenario
Some date are loaded into a program (e.g., evaluation of students in a class where each student is a distinct entity with his/her evaluation data) and a summary of them is shown on a datagrid. The user selects selects some of the students, and performs an analysis on their evaluation. The analysis process requires some parameters, therefore before analysis a window pops-up and lets user to specify his preferred parameters; then the analysis process executes.
Implementation summary
The datagrid is defined as following and binded to a ViewModel:
<DataGrid x:Name="CachedSamplesDG" ItemsSource="{Binding cachedDataSummary}">
<DataGrid.Columns>
<DataGridTextColumn Header="name" Binding="{Binding name}"/>
<DataGridTextColumn Header="score" Binding="{Binding score}"/>
</DataGrid.Columns>
</DataGrid>
The button that starts the process is defined as following:
<Button x:Name="AnalysisBT" Content="Analyze" Command="{Binding AnalyzeCommand}" CommandParameter="{Binding ElementName=CachedSamplesDG, Path=SelectedItems}"/>
The ViewModel is pretty basic and summarized as following:
internal class CachedDataSummaryViewModel
{
public CachedDataSummaryViewModel()
{
_cachedDataSummary = new ObservableCollection<CachedDataSummary>();
AnalyzeCommand = new SamplesAnalyzeCommand(this);
}
private ObservableCollection<CachedDataSummary> _cachedDataSummary;
public ObservableCollection<CachedDataSummary> cachedDataSummary { get { return _cachedDataSummary; } }
public ICommand AnalyzeCommand { get; private set; }
}
And here is the definition of analysis command:
internal class SamplesAnalyzeCommand : ICommand
{
public SamplesAnalyzeCommand(CachedDataSummaryViewModel viewModel)
{
_viewModel = viewModel;
}
private CachedDataSummaryViewModel _viewModel;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
// canExecute logic
}
public void Execute(object parameter)
{
// process mess ...
// Here I need the selected rows of datagird, which "parameter" delegates them.
// I also need some other parameters for analysis which user can set through another view
}
}
An this is a diagram of my current process and what I would like to do next
Question
When the button is clicked
Apply some UI changes on MainWindow
Pop-up ProcessOptionsWindow
Get set parameters from ProcessOptionsWindow
Pass the selected datagrid rows and user specified parameters to SamplesAnalyzeCommand
What would be the best way to achieve this requirement ?
simply use a dialogservice like Good or bad practice for Dialogs in wpf with MVVM?.
then you can do something like this in your ViewModel
var result = this.uiDialogService.ShowDialog("Prozess Options Window", prozessOptionVM);
...
var parameter1 = prozessOptionVM.Parameter1;
You can define another Model and ViewModel for Process Options, and then in the SamplesAnalyzeCommand, display the ProcessOptionsView. When user is done with the ProcessOptionsView, the main ViewModel gets notified (e.g by an event handler) and completes the Process.
Something like this:
internal class SamplesAnalyzeCommand : ICommand {
...
public void Execute(object parameter)
{
this._viewModel.ShowProcessOptions(parameter);
}
}
internal class CachedDataSummaryViewModel {
public string Status {
get {
return this.status;
}
set {
if (!string.Equals(this.status, value)) {
this.status = value;
// Notify property change to UI
}
}
}
...
internal void ShowProcessOptions(object paramter) {
// Model
var processOptions = new ProcessOptionsModel() {
otherInfo = parameter
};
// View-Model
var processOptionsViewModel = new ProcessOptionsViewModel();
processOptionsViewModel.Model = processOptions;
// View
var processOptionsView = new ProcessOptionsView(
processOptionsViewModel
);
// Edit2: Update status
this.Status = "Selecting process options...";
// You can use the event handler or dialog result
processOptionsViewModel.OK += this.PerformProcess;
processOptionsView.ShowDialog();
}
private void PerformProcess(object sender, EventArgs e) {
var processOptionsView = sender as ProcessOptionsView;
var processOptionsModel = processOptionsView.Model;
var processOptions = processOptionsModel.Model;
// Edit2: Update status
this.Status = "Performing process...";
// use processOptions.OtherInfo for initial info
// use processOptions.* for process options info
// and perform the process here
// Edit2: Update status
this.Status = "Process Done.";
}
...
}
class ProcessOptionsModel {
public object OtherInfo {
get;
set;
public int Parameter1 {
get;
set;
}
public IList<ProcessItem> SelectedItems {
get;
set;
}
...
}
class ProcessOptionsViewModel {
public event EventHandler OK;
private SamplesAnalyzeCommand model;
private ICommand okCommand;
public ProcessOptionsViewModel() {
this.okCommand = new OKCommand(this.OnOK);
}
public SamplesAnalyzeCommand Model {
get {
return model;
}
set {
this.model = value;
// Property changed stuff here
}
}
private void OnOK(object parameter) {
if (this.OK != null) {
this.OK = value;
}
}
}
class ProcessOptionsView {
// Interacts with it's view-model and performs OK command if
// user pressed OK or something
}
Hope it helps.
Edit (1):
As blindmeis suggested, you may use some Dialog Service to make the connection between the views.
Edit (2):
Immidiate UI changes after button click can be done in ShowProcessOptions method of the ShowProcessOptions. I don't think you want reflect ui changes of the options window while user works with it, to the main window. UI changes after user closes options window can be done in PerformProcess.
If you want to make an abstraction for options selection (e.g reading from a file) as you mentioned in the comment below, you may define an IOptionsProvider interface, and put ProcessOptionsView and View-Model behind that but still you use the same model.
interface IOptionsProvider {
ProcessOptionsModel GetProcessOptions();
}
class ProcessOptionsView : IOptionsProvider {
public ProcessOptionsModel GetProcessOptions() {
if (this.ShowDialog()) {
return this.ModelView.Model;
}
return null;
}
}
class ProcessOptionsFromFile : IOptionsProvider {
public ProcessOptionsModel GetProcessOptions() {
// Create an instance of ProcessOptionsModel from File
}
}
Note that in this case I removed the OK event since the GetProcessOptions is supposed to block until user closes the main window. If you want a responsive approach in the FromFile case, you may need to work on the async stuff, maybe define GetProcessOptionsAsync instead.
In this case things may get a little bit complicated but I guess it is achievable in this way.
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);
}