Loading data incrementally in Collection view - c#

I am trying to load data incrementally but the RemainingItemsThresholdReachedCommand is not triggered by RemainingItemsThreshold
Here the doc related to this : https://learn.microsoft.com/fr-fr/xamarin/xamarin-forms/user-interface/collectionview/populate-data
:
here my collection view page :
<ContentPage.Content>
<StackLayout BackgroundColor="#b4b4b4">
<RefreshView Command="{Binding RefreshItemsCommand}" IsRefreshing="{Binding IsRefreshing}" >
<CollectionView x:Name="CollectionViewThumbnails" SelectionMode="Multiple" Margin="13" ItemsSource="{Binding Items}"
RemainingItemsThresholdReachedCommand="{Binding ItemTresholdReachedCommand}"
RemainingItemsThreshold="{Binding ItemTreshold}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="1" VerticalItemSpacing="13" HorizontalItemSpacing="13"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:Name="data">
<Frame >
<StackLayout>
<Image Source="{Binding EmplacementThumbnails}" Aspect="AspectFill"/>
</StackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
<ActivityIndicator IsRunning="{Binding IsBusy}"
HeightRequest="30"
HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="30"/>
<StackLayout>
<Button Text="Go" Clicked="StartProcessConcatePages"/>
</StackLayout>
</StackLayout>
</ContentPage.Content>
My code behind :
public partial class ConcatePageThumbnails : ContentPage
{
FileEndpoint fileEndpoint = new FileEndpoint();
FileInfo fileInfo;
BitmapDataStore viewModel;
protected async override void OnAppearing()
{
base.OnAppearing();
if (viewModel.Items.Count == 0)
viewModel.LoadItemsCommand.Execute(null);
MessagingCenter.Subscribe<object, ThumbnailsModel>(this, BitmapDataStore.ScrollToPreviousLastItem, (sender, item) =>
{
CollectionViewThumbnails.ScrollTo(item, ScrollToPosition.End);
});
}
//TODO -- Gerer le retour
public ConcatePageThumbnails(FileInfo fileInfo)
{
InitializeComponent();
this.fileInfo = fileInfo;
BindingContext = viewModel = new BitmapDataStore(fileInfo);
}
}
And my view model :
public class BitmapDataStore
{
IGetThumbnails getThumbnails;
FileInfo fileInfo;
public List<ThumbnailsModel> Items { get; set; }
public Command LoadItemsCommand { get; set; }
public Command ItemTresholdReachedCommand { get; set; }
public Command RefreshItemsCommand { get; set; }
public const string ScrollToPreviousLastItem = "Scroll_ToPrevious";
private int _itemTreshold;
private bool _isRefreshing;
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { isBusy = value; }
}
public bool IsRefreshing
{
get { return _isRefreshing; }
set { _isRefreshing = value; }
}
public int ItemTreshold
{
get { return _itemTreshold; }
set { _itemTreshold = value; }
}
public BitmapDataStore(FileInfo fileInfo)
{
ItemTreshold = 2;
Items = new List<ThumbnailsModel>();
this.fileInfo = fileInfo;
getThumbnails = DependencyService.Get<IGetThumbnails>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
ItemTresholdReachedCommand = new Command(async () => await ItemsTresholdReached());
RefreshItemsCommand = new Command(async () =>
{
await ExecuteLoadItemsCommand();
IsRefreshing = false;
});
}
async Task ItemsTresholdReached()
{
if (IsBusy)
return;
IsBusy = true;
try
{
var items = await getThumbnails.GetBitmaps(fileInfo.FullName, Items.Count);
var previousLastItem = Items.Last();
foreach (var item in items)
{
Items.Add(item);
}
Debug.WriteLine($"{items.Count()} {Items.Count} ");
if (items.Count() == 0)
{
ItemTreshold = -1;
return;
}
MessagingCenter.Send<object, ThumbnailsModel>(this, ScrollToPreviousLastItem, previousLastItem);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
async Task ExecuteLoadItemsCommand()
{
if (IsBusy)
return;
IsBusy = true;
try
{
ItemTreshold = 2;
Items.Clear();
var items = await getThumbnails.GetBitmaps(fileInfo.FullName, Items.Count);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
My Dependencyservice class where I get the thumbnails and The items for my Collection view :
public async Task<List<ThumbnailsModel>> GetBitmaps(string filePath, int lastIndex = 0)
{
var sw = new Stopwatch();
sw.Start();
int numberOfItemsPerPage = 6;
PdfRenderer pdfRenderer = new PdfRenderer(GetSeekableFileDescriptor(filePath));
var appDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath);
string directoryPath = System.IO.Path.Combine(appDirectory, "thumbnailsTemp", System.IO.Path.GetFileNameWithoutExtension(fileName));
List<ThumbnailsModel> thumbnailsModels = new List<ThumbnailsModel>();
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
//int pageCount = pdfRenderer.PageCount;
for (int i = lastIndex; i < lastIndex + numberOfItemsPerPage; i++)
{
PdfRenderer.Page page = pdfRenderer.OpenPage(i);
Android.Graphics.Bitmap bmp = Android.Graphics.Bitmap.CreateBitmap(page.Width, page.Height, Android.Graphics.Bitmap.Config.Argb4444);
page.Render(bmp, null, null, PdfRenderMode.ForDisplay);
try
{
using (FileStream output = new FileStream(System.IO.Path.Combine(directoryPath, fileName + "Thumbnails" + i + ".png"), FileMode.Create))
{
bmp.Compress(Android.Graphics.Bitmap.CompressFormat.Png, 0, output);
}
page.Close();
}
catch (Exception ex)
{
//TODO -- GERER CETTE EXPEXPTION
throw new Exception();
}
}
int y = 1;
Directory.GetFiles(directoryPath).ToList<string>().Skip(lastIndex).Take(numberOfItemsPerPage).ForEach(delegate (string thumbnailsEmplacement)
{
thumbnailsModels.Add(new ThumbnailsModel(y, thumbnailsEmplacement));
y++;
});
sw.Stop();
return await Task.FromResult(thumbnailsModels);
}
I tried to change the ItemTreshold value but doesn't work... Any idea ?
Update :
With different test I saw that ItemTreshold and RemainingItemsThresholdReachedCommand are triggered but not as expected.
My collection view is loading with the number of item set but When I am going down and I am triggering RemainingItemsThresholdReachedCommand this method is triggered consecutively
For example I have 30 Pages : the first 6 pages are loading but when RemainingItemsThresholdReachedCommand is triggered for the first time, and again and again until all the pages and items are done. Then nothing happen on the ui collection view screen.

I just started from sratch with this sample and now it work : https://forums.xamarin.com/discussion/comment/402926#Comment_402926

Add "IsBusy" check in starting of "RemainingItemsThresholdReachedCommand". It will stop multi async calls.
if (IsBusy) return

Related

C# WPF ComboBox ItemsSource Binding Issue

I have an issue with binding ComboBox ItemsSource with a list. I read workplaces from csv file. It can't see the Workplaces list. Please, tell me what is wrong.
xaml:
<ComboBox Grid.Column="1" Grid.Row="9" Height="25" Margin="0,18,0,0" ItemsSource="{Binding Workplaces}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding title}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
xaml.cs:
public partial class MainWindow : Window
{
private BindableCollection<WorkplaceInfo> Workplaces { get; set; }
public MainWindow()
{
InitializeComponent();
Workplaces = new BindableCollection<WorkplaceInfo>(GetWorkplaces());
}
private List<WorkplaceInfo> GetWorkplaces()
{
List<WorkplaceInfo> workplaces = new List<WorkplaceInfo>();
using (var streamReader = new StreamReader("Workplaces.csv"))
{
using (var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture))
{
//csvReader.Context.RegisterClassMap<WorkplaceInfoClassMap>();
var workplaceInfoList = csvReader.GetRecords<dynamic>().ToList();
foreach (var wi in workplaceInfoList)
{
workplaces.Add(new WorkplaceInfo(wi.title, wi.member_of.Split(";")));
}
}
}
return workplaces;
}
}
WorkplaceInfo class:
class WorkplaceInfo
{
public String title { get; }
public String[] memberOfList { get; }
public WorkplaceInfo(string title, string[] memberOfList)
{
this.title = title;
this.memberOfList = memberOfList;
}
}
Here is your code optimized to work:
public ObservableCollection<WorkplaceInfo> Workplaces { get; set; }
public MainWindow()
{
this.DataContext = this;
Workplaces = new ObservableCollection<WorkplaceInfo>(GetWorkplaces());
InitializeComponent();
}
private List<WorkplaceInfo> GetWorkplaces()
{
List<WorkplaceInfo> workplaces = new List<WorkplaceInfo>();
try
{
using (var streamReader = new StreamReader("Workplaces.csv"))
{
using (var csvReader = new CsvReader(streamReader, CultureInfo.CurrentCulture))
{
//csvReader.Context.RegisterClassMap<WorkplaceInfoClassMap>();
var workplaceInfoList = csvReader.GetRecords<dynamic>().ToList();
foreach (var wi in workplaceInfoList)
{
var titl = wi.title;
workplaces.Add(new WorkplaceInfo(wi.title, new List<string>() { wi.member_of }.ToArray()));
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
return workplaces;
}
So the changes needed in your code are:
Set your Workplaces collection to public ObservableCollection
Add DataContext binding
Read in the Date and create the collection before the main window is initialized (other way round the UI will not detect the change in you object unless you implement INotifyPropertyChanged event)
p.s. I don't know the structure of your csv file so I made my small demo like (Workplaces.csv) and adopted the parser. You can keep your parser if it matches your csv file structrue.:
title;member_of
London;First
Amsterdam;Second
And my warm recommendation is to use try-catch block always when handling files and when working with anything what is external to your application.
Best regards.

How to achieve Pagination in Xamarin forms

I have a requirement to implement a paging within a List View page. Here I am displaying hundreds or thousands of records in a table hence the need for paging functionality.
An infinite scrolling down approach is not viable due to the number of records. Same as attached image.
<ScrollView>
<SomeDataContentView Data={Binding SelectedData} />
</ScrollView>
<StackLayout BindableLayout.ItemSource={Binding ListOfPageNumber}
x:Name="SomeStack"
Orientation="Horizontal">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Button Text={Binding .}
Command={Binding Path=BindingContext.SelectPage_Command, Source={x:Reference SomeStack}
CommandParameter={Binding .}
</DataTemplate>
</BindableLayout.ItemTemplate>
ViewModel:
public SelectPage_Command = new Command((param) =>
{
var pageNo = (int)param;
SelectedData = DataDividedByPage[pageNo];
});
SomeDataContentView.cs:
SomeDataContentView : ContentView
public static readonly BindableProperty DataProperty = BindableProperty.Create(
nameof(Data), typeof(string), typeof(SomeDataContentView ), defaultValue: "", propertyChanged: DoSomethingWhenDataChanged);
public string Data
{
get => (string)GetValue(DataProperty perty);
set => SetValue(DataProperty , value);
}
And create your SomeDataContentView.xaml to show the data how you want it.
This is probably how I'd do it.
I've done it from memory and skipped one or two things. But I'll come back to it later, if you need me to.
//How to add Pagination in Xamarin Forms without using ViewModel
//Step 1-> First , we will add ListView property -> ItemAppearing
//In EmployeeTestListing.xaml page , we need to write
<ListView x:Name="lstEmployee"
IsPullToRefreshEnabled="True"
ItemTapped="lstEmployee_ItemTapped"
Refreshing="lstEmployee_Refreshing"
ItemAppearing="lstEmployee_ItemAppearing"
HasUnevenRows="True">
//Step 2-> In EmployeeTestListing.xaml.cs page , we need to write
public partial class EmployeeTestListing : ContentPage
{
ObservableCollection<EmployeeTestListingModel> employeeList;
public EmployeeTestListing()
{
InitializeComponent();
employeeList = new ObservableCollection<EmployeeTestListingModel>(); // Pagination
lstEmployee.ItemsSource = employeeList; // Pagination
}
public void LoadData()
{
var response = await httpService.Post("EmployeeTest/getemployeetests", employeeTest, false);
if (response.IsSuccessStatusCode)
{
var responseUserDataString = await response.Content.ReadAsStringAsync();
RootEmployeeTestListingModel employeeTestClass = JsonConvert.DeserializeObject<RootEmployeeTestListingModel>(responseUserDataString);
if (employeeTestClass.data.Count == 0)
{
loader.IsVisible = false;
loader.IsRunning = false;
lstEmployee.IsVisible = false;
lblNoData.IsVisible = true;
}
else
{
//For Pagination include below foreach code
foreach (var item in employeeTestClass.data)
{
employeeList.Add(item);
}
lstEmployee.IsVisible = true;
loader.IsVisible = false;
loader.IsRunning = false;
lblNoData.IsVisible = false;
}
}
}
//Pagination Scrolling Method ItemAppearing
private async void lstEmployee_ItemAppearing(object sender, ItemVisibilityEventArgs e)
{
if (employeeList.Count == 0) return;
try
{
var item = (EmployeeTestListingModel)e.Item;
if (employeeList.Count >= pageSize)
{
if (item == employeeList.Last())
{
pageSize += pageSize;
lstEmployee.IsVisible = true;
await LoadData();
}
}
}
catch
{
}
}
}

ISupportIncrementalLoading loads more items than required

In my UWP I have a ListView that is populated incrementally using the ISupportIncrementalLoading Interface for infinite scrolling.
This list is on a page PageX and as soon as I navigate to this page, the ListView gets populated.
This somethimes works and sometimes doesn't. problem is caused when I navigate to PageX, the LoadMoreItemsAsync is called more than once ( for the first time only, for further scrolling it works fine ).
Here's my code:
public class ItemsToShow : ObservableCollection<SearchResultViewModel>, ISupportIncrementalLoading
{
private SearchResponse ResponseObject { get; set; } = new SearchResponse();
private MetadataReply Metadata { get; set; } = new MetadataReply();
SearchResultViewModel viewModel = null;
public bool HasMoreItems
{
get
{
if ((string.IsNullOrEmpty(SomeStaticClass.NextPageToken) && !SomeStaticClass.IsFirstRequest) || SomeStaticClass.StopIncrementalLoading)
return false;
if(SomeStaticClass.IsFirstRequest)
{
using (var db = new DbContext())
{
var json = db.UpdateResponse.First(r => r.LanguageId == DataStore.Language).JsonResponse;
Metadata = Newtonsoft.Json.JsonConvert.DeserializeObject<UpdateApiResponse>(json).response.metadata.reply;
}
var returnObject = SomeStaticClass.SearchResponse;
ResponseObject = returnObject.response;
}
else
{
var returnObject = new SearchApiCall().CallSearchApiAsync(
SomeStaticClass.QueryString,
SomeStaticClass.NextPageToken,
SomeStaticClass.Filters).Result;
ResponseObject = returnObject.response;
}
return ResponseObject.documents.Count > 0;
}
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
CoreDispatcher coreDispatcher = Window.Current.Dispatcher;
if (SomeStaticClass.IsFirstRequest) SomeStaticClass.Facet = ResponseObject.facets;
return Task.Run<LoadMoreItemsResult>(async () =>
{
await coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
foreach (var item in ResponseObject.documents)
{
this.Add(new SearchResultViewModel { .... });
}
});
SomeStaticClass.IsFirstRequest = false;
SomeStaticClass.NextPageToken = ResponseObject.pageToken;
await coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
SearchResultPage.searchResultPage.FilterButton.Visibility = Visibility.Visible;
});
return new LoadMoreItemsResult() { Count = count };
}).AsAsyncOperation<LoadMoreItemsResult>();
}
}
My ListView:
<ListView Name="SearchResultListView"
SelectionMode="Single"
IsItemClickEnabled="True"
ItemClick="SearchResultListView_ItemClick">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="ViewModels:SomeViewModel">
<Grid Style="{StaticResource SomeStyle}">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Text="{x:Bind qqq}/>
<StackPanel Grid.Row="1">
<TextBlock Text="{x:Bind xxx}"/>
<TextBlock Text="{x:Bind yyy}"/>
</StackPanel>
<StackPanel Grid.Row="2">
<TextBlock Text="{x:Bind aaa}"/>
<TextBlock Text="{x:Bind bbb}" />
</StackPanel>
<TextBlock Text="{x:Bind EducationalLevel}"/
<StackPanel Grid.Row="4">
<TextBlock Text="{x:Bind Language}"/>
<Image Source="{x:Bind ccc}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Code behind of the page that has this ListView:
public sealed partial class SearchResultPage
{
public static SearchResultPage searchResultPage { get; private set; }
private SearchResultParameterWrapper ReceivedParameter { get; set; } = new SearchResultParameterWrapper();
public SearchResultPage()
{
InitializeComponent();
NavigationCacheMode = NavigationCacheMode.Enabled;
searchResultPage = this;
}
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.NavigationMode != NavigationMode.Back) FilterButton.Visibility = Visibility.Collapsed;
SomeStaticClass.IsFirstRequest = true;
SomeStaticClass.Filters = new FilterParametersWrapper();
ReceivedParameter = (SearchResultParameterWrapper)e.Parameter;
if (ReceivedParameter != null)
{
SomeStaticClass.QueryString = ReceivedParameter.QueryString;
SomeStaticClass.Filters = ReceivedParameter.Filters;
SomeStaticClass.RestoreOldFilters = ReceivedParameter.RestoreOldFilters;
if(SomeStaticClass.IsFirstRequest)
await HandleNoResult(ReceivedParameter);
}
}
private async Task HandleNoResult(SearchResultParameterWrapper parameter)
{
if (!ApiStore.IsConnected())
{
Toast.ShowToast(MainPage.mainPage.ViewModel._APP_check_network, ToastRow);
return;
}
MyProgressRing.IsActive = true;
SearchResultListView.ItemsSource = null;
SearchResultListView.Items.ToList().Clear();
SearchResponse responseObject = null;
SearchApiResponse apiResponse = null;
try
{
SomeStaticClass.StopIncrementalLoading = true;
SomeStaticClass.SearchResponse = await new SearchApiCall().CallSearchApiAsync(parameter.QueryString, null, parameter.Filters);
apiResponse = SomeStaticClass.SearchResponse;
responseObject = apiResponse.response;
if (responseObject.documents.Count <= 0)
{
NoResultsTextBlock.Visibility = Visibility.Visible;
FilterButton.Visibility = Visibility.Collapsed;
return;
}
else
{
SearchResultListView.ItemsSource = new ItemsToShow();
SomeStaticClass.StopIncrementalLoading = false;
}
}
catch
{
}
finally
{
MyProgressRing.IsActive = false;
}
}
public bool Reload() { return Reload(null); }
private bool Reload(object param)
{
System.Type type = Frame.CurrentSourcePageType;
if (Frame.BackStack.Any())
{
param = ReceivedParameter;
}
try { return Frame.Navigate(type, param); }
finally { Frame.BackStack.Remove(Frame.BackStack.Last()); }
}
}
EDIT:
I've update my code. Same problem remains and another problem arises ie, The page sometimes goes blank ( after showing items for one or two seconds):
public class ItemsToShow : ObservableCollection<SearchResultViewModel>, ISupportIncrementalLoading
{
private SearchResponse ResponseObject { get; set; } = new SearchResponse();
private bool hasMoreItems { get; set; } = true;
public bool HasMoreItems
{
set
{
hasMoreItems = value;
}
get
{
if (SomeCondition) return false;
return hasMoreItems;
}
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
CoreDispatcher coreDispatcher = Window.Current.Dispatcher;
Task.Delay(10);
return Task.Run<LoadMoreItemsResult>(async () =>
{
if (IsFirstRequest)
{
HasMoreItems = string.IsNullOrEmpty(ResponseObject.someProperty) ? false : true;
IsFirstRequest = false;
}
else
{
ResponseObject = await new SomeClass().SomeMethod();
HasMoreItems = string.IsNullOrEmpty(ResponseObject.someProperty) ? false : true;
}
await coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
foreach (var item in ResponseObject.documents)
{
this.Add(PrepareViewModel(item));
}
});
await coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
SearchResultPage.searchResultPage.FilterButton.Visibility = Visibility.Visible;
});
return new LoadMoreItemsResult() { Count = count };
}).AsAsyncOperation<LoadMoreItemsResult>();
}
}
As long as you don't try to load the same items twice, I don't really see much issue with it. It's like having two sets of data on load, and then loading one set on scroll.
But you probably would wonder why it's doing this, and here is how I understand it.
During the initial load, ISupportIncrementalLoading.LoadMoreItemsAsync will always be called more than once unless there's no data to populate. This behavior is most likely caused by the design of ListView virtualization.
My guess is that before the ListView decides how many items to load initially, it needs to know the item height in order to calculate the number of items to virtualize. Therefore, it first loads one item to get the height, do the calculations and call LoadMoreItemsAsync again. This is why if you put a breakpoint inside LoadMoreItemsAsync, you will see the count value always gives you 1 the first time the method is called.
To avoid this behavior, you cannot simply ignore the first call 'cause incremental loading won't be triggered when there's no items in view. So you can hack around to ignore not the first but second call.
Take the source from UWP Community Toolkit for example, you can do something like this -
private bool? _firstLoadJustPassed;
private async Task<LoadMoreItemsResult> LoadMoreItemsAsync(uint count, CancellationToken cancellationToken)
{
if (_firstLoadJustPassed == true)
{
_firstLoadJustPassed = false;
return new LoadMoreItemsResult { Count = 0 };
}
uint resultCount = 0;
_cancellationToken = cancellationToken;
try
{
if (!_cancellationToken.IsCancellationRequested)
{
IEnumerable<IType> data = null;
try
{
IsLoading = true;
data = await LoadDataAsync(_cancellationToken);
}
catch (OperationCanceledException)
{
// The operation has been canceled using the Cancellation Token.
}
catch (Exception ex) when (OnError != null)
{
OnError.Invoke(ex);
}
if (data != null && data.Any() && !_cancellationToken.IsCancellationRequested)
{
resultCount = (uint)data.Count();
foreach (var item in data)
{
Add(item);
}
if (!_firstLoadJustPassed.HasValue)
{
_firstLoadJustPassed = true;
}
}
else
{
HasMoreItems = false;
}
}
}
finally
{
IsLoading = false;
if (_refreshOnLoad)
{
_refreshOnLoad = false;
await RefreshAsync();
}
}
return new LoadMoreItemsResult { Count = resultCount };
}
There might be more elegant solutions than this but again like I said, I personally don't see such a big issue with this behavior and I would use IncrementalLoadingCollection from the toolkit than writing my own implementation.

Calling a web service via an Async Task from UI thread is causing a deadlock

I am a newbie to Xamarin Forms and have an App that fetch's Transactions asynchronously but it encounters a Deadlock when I call a web service.
I have a TransactionView:
This is the XAML:
<Label Text="Account:" Grid.Row="0" Grid.Column="0" Style="{StaticResource LabelStyle}"/>
<ctrls:BindablePicker x:Name="ddlAccountsWinPhone" ItemsSource="{Binding Accounts}" SelectedIndex="{Binding AccountID}" Grid.Row="0" Grid.Column="1" />
<Label Text="From Date:" Grid.Row="2" Grid.Column="0" Style="{StaticResource LabelStyle}"/> <DatePicker x:Name="dtFromWinPhone" Date="{Binding FromDate}" Grid.Row="2" Grid.Column="1" />
<Label Text="To Date:" Grid.Row="3" Grid.Column="0" Style="{StaticResource LabelStyle}"/> <DatePicker x:Name="dtToWinPhone" Date="{Binding ToDate}" Grid.Row="3" Grid.Column="1" />
<Button x:Name="btnViewWinPhone" Text="View" Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command ="{Binding ShowTransactions}" />
This is the TransactionView's code behind, note I'll show a grid of transactions on subpage called TransactionSubPageView:
public partial class TransactionView : ContentPage
{
private TransactionViewModel transactionViewModel;
private TransactionViewModel ViewModel
{
get {
if (transactionViewModel == null) transactionViewModel = new TransactionViewModel(
this.Navigation, new TransactionSubPageView()); //Pass sub page to ViewModel ctor
return transactionViewModel;
}
}
public TransactionView()
{
InitializeComponent();
BindingContext = ViewModel;
}
Here is the TransactionViewModel:
public class TransactionViewModel : ViewModelBase
{
private int accountID;
private List<Account> accounts = new List<Account>();
private Account accountItemSelected;
private DateTime fromDate = new DateTime(1900,1,1);
private DateTime toDate = new DateTime(1900, 1, 1);
private ContentPage transactionGridViewNav;
public TransactionViewModel(INavigation navigationInterface, ContentPage transactionSubPageView)
{
base.navigation = navigationInterface;
transactionSubPageViewNav = transactionSubPageView; //I SAVE A REFERENCE TO THE SUBPAGE HERE
accounts.AddRange(Client.Accounts);
}
public ICommand ShowTransactions
{
get
{
return new Command(async () =>
{
//HERE IS WHERE "I THINK" I WANT TO FETCH THE TRANSACTIONS
//THEN NAVIGATE TO THE THE SUBPAGE WITH THE GRID OF TRANSACTIONS
await navigation.PushAsync(transactionSubPageViewNav);
});
}
}
public int AccountID
{
get{ return this.accountID; }
set{
this.accountID = value;
this.OnPropertyChanged("AccountID");
}
}
public List<Account> Accounts
{
get{
return this.accounts;}
set{
this.accounts = value;
this.OnPropertyChanged("Accounts");
}
}
public Account AccountItemSelected
{
get{return accountItemSelected;}
set {
if (accountItemSelected != value)
{
accountItemSelected = value;
OnPropertyChanged("AccountItemSelected");
}
}
}
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }}
...
This is the TransactionSubPageView:
public partial class TransactionSubPageView : ContentPage
{
public TransactionSubPageViewModel transactionSubPageViewModel;
public TransactionSubPageViewModel ViewModel
{
get
{
if (transactionSubPageViewModel == null)
transactionSubPageViewModel = new TransactionSubPageViewModel();
return transactionGridViewModel;
}
}
private Grid gridTransactions;
public TransactionSubPageView()
{
InitializeComponent();
BindingContext = ViewModel;
}
protected async override void OnAppearing()
{
base.OnAppearing();
//THIS IS A VOID TO POPULATE A GRID AND SET THE PAGE'S CONTENT, IT USES
//transactionSubPageViewModel.TransactionsGrid!!
PopulateGridTransactions();
}
This is the SubPage ViewModel:
public class TransactionSubPageViewModel : ViewModelBase
{
public List<Transaction> transactionsGrid = new List<Transaction>();
public int accountId = 1636;
public DateTime fromDate = new DateTime(2015, 8, 1);
public DateTime toDate = new DateTime(2015, 9, 1);
public TransactionGridViewModel() { }
public List<Transaction> TransactionsGrid
{
get {
if (transactionsGrid == null) transactionsGrid = MyWebService.GetTransactions(1636, new DateTime(2015, 8, 1), new DateTime(2015, 9, 1)).Result;
return transactionsGrid;}
}
}
Lastly here is the WebService Call which is causing the problem:
public static async Task<List<Transaction>> GetTransactions(int accountId, DateTime fromDate, DateTime toDate)
{
var client = new System.Net.Http.HttpClient(new NativeMessageHandler());
client.BaseAddress = new Uri("http://theWebAddress.com/);
var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd")); //.ConfigureAwait(false);
var transactionJson = response.Content.ReadAsStringAsync().Result;
var transactions = JsonConvert.DeserializeObject<List<Transaction>>(transactionJson);
return transactions;
}
Thanks for reading so far, the problem is this line in the webmethod call always hangs:
var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd"));
If I call the GetTransactions webservice from the SubPage's OnAppearing event it hangs, if I call it from ICommand ShowTransactions it also hangs. Am I missing an await or a continue?
I've read a fair few documents with similar people and who are confused, I know I am encountering a deadlock but I just don't know how to fix it.
I've tried ConfigureAwait(false) without luck. It would be ideal if I could just call the WebService on a background thread, show a progress bar and when the operation is complete render the results in the page.
Success!! I knew this would be a case of asking here and then I would work it out. Funny how that works!
This is how I got it working feel free to critique it:
I turned the TransactionSubPageViewModel's TransactionGrid property into an Async method and put the webservice call in there directly:
public async Task<List<Transaction>> TransactionsGrid()
{
if (transactionsGrid == null)
{
var client = new System.Net.Http.HttpClient(new ModernHttpClient.NativeMessageHandler());
client.BaseAddress = new Uri("http://theWebAddress.com/);
var response = await client.GetAsync("API.svc/Transactions/" + accountId + "/" + fromDate.ToString("yyyy-MM-dd") + "/" + toDate.ToString("yyyy-MM-dd"));
var transactionJson = await response.Content.ReadAsStringAsync(); //DONT USE Result HERE, USE AWAIT AS PER #Noseratio suggested
var transactions = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Transaction>>(transactionJson);
transactionsGrid = transactions;
}
return transactionsGrid;
}
Then I called the web service from where I thought I wanted to call it.
public ICommand ShowTransactions
{
get
{
return new Command(async () =>
{
//THIS IS WHERE I WAS MISSING THE AWAIT!!!
await ((TransactionGridViewModel)transactionSubPageViewNav.BindingContext).TransactionsGrid();
await navigation.PushAsync(transactionSubPageViewNav);
});
}
Then when I call the PopulateGridTransactions from the OnAppearing() the data is already available:
public void PopulateGridTransactions()
{
...
foreach (Models.Transaction transaction in transactionSubPageViewModel.TransactionsGrid().Result)
{
Edit:
As #Noseratio points out, let me explain why you need a Try/Catch in the async.
Coincidentally after getting a response from the webservice I got an error on the next line when I deserialized the json web service result.
I'd setup a global exception handler in the Xamarin App.cs so it was catching it:
By putting a Try/Catch in the async I can catch the exception on the same frame (before the stack has been unwound when it is caught by the Application_UnhandledException - which is very annoying to track down ):

DevExpress - MVVM - Generate TabItems with different ViewModels

i have a DXTabControl. The DXTabItems are generated via my ViewModel.
//MainViewModel
public MainViewModel()
{
var items = new ObservableCollection<DXTabItem>();
items.Add(
new DXTabItem()
{
Header = "Test1",
Content = new WebViewModel()
});
items.Add(
new DXTabItem()
{
Header = "Test2",
Content = new CMSViewModel()
});
TabItems = items;
}
private ObservableCollection<DXTabItem> _tabItems;
public ObservableCollection<DXTabItem> TabItems
{
get { return _tabItems; }
set { SetProperty(ref _tabItems, value, () => TabItems); }
}
I am working with a DataTemplate and my TabItem is still not showing any UserControl.
//MainView.xaml
<DataTemplate x:Key="WebTemplate" DataType="{x:Type viewmodel:WebViewModel}">
<view:WebView/>
</DataTemplate>
<DataTemplate x:Key="CMSTemplate" DataType="{x:Type viewmodel:CMSViewModel}">
<view:CMSView/>
</DataTemplate>
<datatemplate:TemplateSelector x:Key="DataTemplateSelector"
WebTemplate="{StaticResource WebTemplate}"
CMSTemplate="{StaticResource CMSTemplate}" />
<dx:DXTabControl ItemsSource="{Binding TabItems}" ItemTemplateSelector="{StaticResource DataTemplateSelector}" />
//DataTemplateSelector
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate WebTemplate { get; set; }
public DataTemplate CMSTemplate { get; set; }
public override DataTemplate SelectTemplate(Object item,
DependencyObject container)
{
if (item == null) return base.SelectTemplate(item, container);
if (item.GetType() == typeof(WebViewModel))
{
return WebTemplate;
}
else if (item.GetType() == typeof(CMSViewModel))
{
return CMSTemplate;
}
else return base.SelectTemplate(item, container);
}
}
Everything is working, except showing the content i need. No view is been shown. Any idea? Did i miss something?
The following answer is based on caliburn.micro.
Step 1: Add a convention to the bootstrapper
public Bootstrapper()
{
ConventionManager.AddElementConvention<DXTabControl>(DXTabControl.ItemsSourceProperty, "ItemsSource", "DataContextChanged")
.ApplyBinding = (viewModelType, path, property, element, convention) =>
{
if (!ConventionManager.SetBindingWithoutBindingOrValueOverwrite(viewModelType, path, property, element, convention, DXTabControl.ItemsSourceProperty))
{
return false;
}
var tabControl = (DXTabControl)element;
if (tabControl.ItemTemplate == null && tabControl.ItemTemplateSelector == null && property.PropertyType.IsGenericType)
{
var itemType = property.PropertyType.GetGenericArguments().First();
if (!itemType.IsValueType && !typeof(string).IsAssignableFrom(itemType))
{
tabControl.ItemTemplate = ConventionManager.DefaultItemTemplate;
}
}
ConventionManager.ConfigureSelectedItem(element, Selector.SelectedItemProperty, viewModelType, path);
if (string.IsNullOrEmpty(tabControl.DisplayMemberPath))
{
ConventionManager.ApplyHeaderTemplate(tabControl, DXTabControl.ItemHeaderTemplateProperty, DXTabControl.ItemHeaderTemplateSelectorProperty, viewModelType);
}
return true;
};
[...]
}
Now you can bind any Screen-Collection to your DXTabControl.
Step 2: Create a collection in the ViewModel
public class MainViewModel : Screen
{
public MainViewModel()
{
DisplayName = "DevExpress Test Environment";
}
private static BindableCollection<Screen> _tbCtrl = new BindableCollection<Screen>();
public BindableCollection<Screen> TbCtrl
{
get { return _tbCtrl; }
set
{
_tbCtrl = value;
NotifyOfPropertyChange(() => TbCtrl);
}
}
}
You can e.g. put any other ViewModel which is based on the Screen class to your collection. That means, you will be able to display your content for each tabitem.
Step 3: Create the DXTabControl in your View (XAML-Code)
<dx:DXTabControl x:Name="TbCtrl" />
Give it a go. Open for feedback.
/// Alternative solution without Caliburn.Micro
Step 1: Add the DXTabControl to your MainView (XAML-Code)
<dx:DXTabControl ItemsSource="{Binding TbCtrlItems}" />
Step 2: Your MainViewModel needs to add those items like i have described above (in my question), but in this case, you have to specify the content-property
public MainViewModel()
{
_tbCtrlItems.Add(new DXTabItem()
{
Header = "Test1",
Content = new Views.View1() {DataContext = new ViewModel1()}
});
_tbCtrlItems.Add(new DXTabItem()
{
Header = "Test2",
Content = new Views.View2() { DataContext = new ViewModel2() }
});
}
private ObservableCollection<DXTabItem> _tbCtrlItems = new ObservableCollection<DXTabItem>();
public ObservableCollection<DXTabItem> TbCtrlItems
{
get { return _tbCtrlItems; }
set { SetProperty(ref _tbCtrlItems, value, () => TbCtrlItems); }
}
I hope this answer is helpful.

Categories

Resources