How to achieve Pagination in Xamarin forms - c#

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
{
}
}
}

Related

C# Xamarin Forms Populating CollectionView from ViewModel is always null

I am trying to populate a collection view from a ViewModel, however when I try to bind the data to the collection view, the ViewModel is null.
xaml.cs file
ObservableCollection<ReportsClass> newKidList = new ObservableCollection<ReportsClass>();
public ReportsViewModel viewmodel { get; set; }
public ReportsPage()
{
InitializeComponent();
viewmodel = new ReportsViewModel();
this.BindingContext = viewmodel;
PreviousDateRange.CornerRadius = 20;
NextDateRange.CornerRadius = 20;
DateTime firstDate = currentDate.StartOfWeek(DayOfWeek.Sunday);
DateTime secondDate = currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday);
DateRange.Text = firstDate.ToString("MMMM d") + " - " + secondDate.ToString("MMMM d");
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(viewmodel.kids));
}
Here is my view model
public class ReportsViewModel
{
public ObservableCollection<ReportsClass> kids { get; set; }
FirebaseStorageHelper firebaseStorageHelper = new FirebaseStorageHelper();
WebServiceClass webServiceClass = new WebServiceClass();
DateTime currentDate = DateTime.Now;
public ReportsViewModel()
{
GetKids();
}
public async void GetKids()
{
var parentId = await SecureStorage.GetAsync("parentid");
kids = await webServiceClass.Reports(Convert.ToInt32(parentId), currentDate.StartOfWeek(DayOfWeek.Sunday), currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday));
}
}
And here is the method that gets the data for the view model
public async Task<ObservableCollection<ReportsClass>> Reports(int parentid, DateTime startDate, DateTime endDate)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("parentid", parentid.ToString()),
new KeyValuePair<string, string>("startDate", startDate.ToString("yyyy-MM-dd H:mm:ss")),
new KeyValuePair<string, string>("endDate", endDate.ToString("yyyy-MM-dd"))
});
var response = await client.PostAsync(string.Format("https://example.com/api/index.php?action=reports"), content);
var responseString = await response.Content.ReadAsStringAsync();
ObservableCollection<ReportsClass> items = JsonConvert.DeserializeObject<ObservableCollection<ReportsClass>>(responseString);
return items;
}
What am I doing wrong? The purpose of me doing it this way is so I can update an item in the collectionview
Here is my ReportsClass
public class ReportsClass
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name { get; set; }
}
OPTION A:
Fix the syntax of Kids.SetBinding, to not get null. Refer to the CLASS ReportsViewModel, not to the INSTANCE viewmodel:
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(ReportsViewModel.kids));
The kids still won't appear in list. To fix, kids needs OnPropertyChanged:
public ObservableCollection<ItemModel> kids {
get => _kids;
set {
_kids = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _kids;
See the other code in Option B. Adapt as desired.
When you need XAML to see a DYNAMIC change, you need OnPropertyChanged. This is an implementation of INotifyPropertyChanged. Add this call to properties (that XAML binds to) of ReportsClass:
// Inheriting from `BindableObject` is one way to obtain OnPropertyChanged method.
public class ReportsClass : Xamarin.Forms.BindableObject
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name {
get => _first_name;
set {
_first_name = value;
// This tells XAML there was a change.
// Makes "{Binding first_name}" work dynamically.
OnPropertyChanged();
}
}
private string _first_name;
}
OPTION B:
Didn't find an answer anywhere that does everything correctly, so here is a complete sample, for future reference:
Remove Kids.SetBinding(...). (It can be fixed as shown in OPTION A, but its easier to get it correct in XAML, so below I show it in XAML.)
Bindings from Page to VM. See xaml below.
Create ObservableCollection with setter that does OnPropertyChanged. This informs XAML when the list is ready, so page updates. (This is an implementation of INotifyPropertyChanged, as Jason mentioned.)
Use Device.BeginInvokeOnMainThread(async () to create an async context, that is queued to run after constructor returns. (This fixes the issue Jason mentioned, which is that a constructor isn't an async context, so should not DIRECTLY call an async method such as QueryItemsAsync, or your GetKids.) This is more reliable.
PageWithQueryData.xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestXFUWP.PageWithQueryData">
<ContentPage.Content>
<StackLayout>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Grid>
<Label Text="Loading ..." FontSize="24" TextColor="Blue" BackgroundColor="LightBlue" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
</Grid>
</CollectionView.EmptyView>
</CollectionView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
PageWithQueryData.xaml.cs:
public partial class PageWithQueryData : ContentPage
{
public PageWithQueryData()
{
InitializeComponent();
// ... other initialization work here ...
// BUT remove `Kids.Binding(...);` line. See XAML: `ItemsSource="{Binding Items}"`.
BindingContext = new VMWithQueryData();
}
}
VMWithQueryData.cs:
class VMWithQueryData : Xamarin.Forms.BindableObject
{
public VMWithQueryData()
{
// Start an async task to query.
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () => {
await QueryItemsAsync();
});
// Alternative implementation: Start a background task to query.
//QueryItemsInBackground();
}
public ObservableCollection<ItemModel> Items {
get => _items;
set {
_items = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _items;
private async Task QueryItemsAsync()
{
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
await Task.Delay(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
await Task.Delay(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
}
// Alternative implementation, using a background thread.
private void QueryItemsInBackground()
{
Task.Run(() => {
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
System.Threading.Thread.Sleep(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
System.Threading.Thread.Sleep(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
});
}
}
ItemModel.cs:
public class ItemModel
{
public ItemModel(string name)
{
Name = name;
}
public string Name { get; set; }
}
This also demonstrates <CollectionView.EmptyView> to display a message to user, while the data is being queried.
For completeness, I've included an alternative QueryItemsInBackground, that uses a background thread instead of an async method. Either approach works well.
Notice inheritance from Xamarin.Forms.BindableObject. This is one way to get an implementation of INotifyPropertyChanged. You can use any other MVVM library or technique.
Move this line of code to the end of your constructor
this.BindingContext = viewmodel;

Xamarin.Forms: Communication between partial view's VM and Parent's VM

I have implemented a partial-view with its own ViewModel so I can use it in different ContentPage (s). However, this partial view must also have some properties that will be bound to the parent's VM. (and this should be without intervention of any third party libs such as Prism)
here is a sample:
PrtialView
PrtialView.xaml:
<StackLayout x:Class="....OtpVerificator" ...>
<Entry x:Name="Otp1Entry"></Entry>
<Entry x:Name="Otp2Entry"></Entry>
<Entry x:Name="Otp3Entry"></Entry>
<Entry x:Name="Otp4Entry"></Entry>
<Entry x:Name="Otp5Entry"></Entry>
<Entry x:Name="Otp6Entry"></Entry>
</StackLayout>
PrtialView.xaml.cs:
public partial class OtpVerificator : StackLayout
{
//.....
// For example I want also set this property from the parent's view or vVM
public static readonly BindableProperty TokenProperty = BindableProperty.Create(nameof(Token), typeof(string), typeof(OtpVerificator), default(string), Xamarin.Forms.BindingMode.TwoWay);
public string Token
{
get
{
return (string)GetValue(TokenProperty);
}
set
{
SetValue(TokenProperty, value);
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TokenProperty.PropertyName)
{
if (Token != EntriesFullText)
{
for (int i = 0; i < Token.Length; i++)
otpEntries[i].Text = Token.Substring(i, 1);
}
}
//......
}
// .......
private string EntriesFullText
{
get => string.Join(string.Empty, otpEntries
.Where(e => !string.IsNullOrEmpty(e.Text))
.Select(e => e.Text[0]));
}
private void OtpEntry_Changed(object sender, TextChangedEventArgs e)
{
if (Token != EntriesFullText)
Token = EntriesFullText;
var oldVal = e.OldTextValue ?? string.Empty;
var newVal = e.NewTextValue ?? string.Empty;
var entry = sender as BorderlessEntry; // .. and check for null
var index = otpEntries.IndexOf(entry); // what if IndexOf returns -1?
// allow for a single val
if (!string.IsNullOrEmpty(entry.Text) && !string.IsNullOrEmpty(oldVal))
entry.Text = newVal.All(nw => nw.ToString() == oldVal) ? oldVal : entry.Text.Replace(oldVal, string.Empty);
if (string.IsNullOrEmpty(entry.Text))
return;
var nextIndex = index + 1;
if (nextIndex >= otpEntries.Length)
entry.Unfocus();
else
{
var next = otpEntries.ElementAt(nextIndex);
next?.Focus();
}
}
}
ParentView.xaml:
I know that Token belongs to the embedded VM, Hence I wrote Token2 to explain that it is different property of the PV that should be able to set from the Parent's view or from parent's VM (binding)
<ContentPage x:Class="....ParentPage">
<partials:OtpVerificator x:Name="otpVerifier"
Grid.Row="2"
HorizontalOptions="CenterAndExpand"
ActionType="LoginConfirmation"
Token2="123658">
</partials:OtpVerificator>
</ContentPage>
Try to give your parentView a name and bind like this :
Token2="{Binding BindingContext.token2 , Source={x:Reference parentView}}"
Here is the example:
<ContentPage x:Class="....ParentPage" x:Name="parentView">
<partials:OtpVerificator x:Name="otpVerifier"
Grid.Row="2"
HorizontalOptions="CenterAndExpand"
ActionType="LoginConfirmation"
Token2="{Binding BindingContext.token2 , Source={x:Reference parentView}}">
</partials:OtpVerificator>
</ContentPage>

Loading data incrementally in Collection view

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

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.

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