Привязка форм Xamarin Пользовательский элемент управления к ViewModel не работает
Я использую Xamarin Forms 2.4 для создания приложений для iOS и Android. Я использую пользовательский элемент управления ListView, чтобы включить бесконечную загрузку элементов, и в нижней части списка есть индикатор активности. У меня есть BindableProperties с именами IsLoadMorePostsAvailable и IsBusy, но после отладки я вижу, что они устанавливаются только при первой загрузке сообщений, но после того, как больше нет сообщений для загрузки, и я устанавливаю эти значения в false, ничего не меняется.
Вот мой пользовательский элемент управления ListView:
using System;
using System.Collections;
using System.Windows.Input;
using Xamarin.Forms;
namespace SOD_APP_V2.Controls
{
/// <summary>
/// A simple listview that exposes a bindable command to allow infinite loading behaviour.
/// Along Auto Disable functionality
/// </summary>
public class InfiniteListView : ListView
{
ActivityIndicator ActivityIndicator_LoadingContents;
public InfiniteListView()
{
ActivityIndicator_LoadingContents = new ActivityIndicator
{
Color = Color.FromHex("#FF3300"),
IsRunning = true,
HorizontalOptions = LayoutOptions.FillAndExpand,
};
// Setting up 'Infinite Scrolling' items
ItemAppearing += AwesomeListView_ItemAppearing;
this.Footer = new StackLayout
{
Orientation = StackOrientation.Vertical,
Children = { ActivityIndicator_LoadingContents },
Padding = new Thickness(0, 5, 0, 5)
};
}
#region UI Control Events
/// <summary>
/// List View ItemAppearing event for infinite scrolling
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AwesomeListView_ItemAppearing(object sender, ItemVisibilityEventArgs e)
{
// Checking whether its possible to load more items
if (!IsLoadMoreItemsPossible)
{
//((InfiniteListView)sender).Footer = null;//te,p
return;
}
var items = ItemsSource as IList;
if (items != null && e.Item == items[items.Count - 1])
{
if (LoadMoreInfiniteScrollCommand != null && LoadMoreInfiniteScrollCommand.CanExecute(null)) {}
LoadMoreInfiniteScrollCommand.Execute(null);
}
}
#endregion
#region Bindable Properties
/// <summary>
/// Gets or sets the property binding that defines whether the list has reached the
/// end of the items source thereby declaring its not possible to load more itmes...
/// </summary>
public static readonly BindableProperty IsLoadMoreItemsPossibleProperty =
BindableProperty.Create(nameof(IsLoadMoreItemsPossible), typeof(bool), typeof(InfiniteListView), default(bool), BindingMode.TwoWay,
propertyChanging: (bindable, oldValue, newValue) =>
{
var ctrl = (InfiniteListView)bindable;
ctrl.IsLoadMoreItemsPossible = (bool)newValue;
});
/// <summary>
/// Gets or sets the property binding that defines whether the listview is busy
/// loading items in the background to the UI
/// </summary>
public static readonly BindableProperty IsBusyProperty =
BindableProperty.Create(nameof(IsBusy), typeof(bool), typeof(InfiniteListView), default(bool), BindingMode.TwoWay,
propertyChanging: (bindable, oldValue, newValue) =>
{
var ctrl = (InfiniteListView)bindable;
ctrl.IsBusy = (bool)newValue;
});
/// <summary>
/// Respresents the command that is fired to ask the view model to load additional data bound collection.
/// </summary>
public static readonly BindableProperty LoadMoreInfiniteScrollCommandProperty =
BindableProperty.Create(nameof(LoadMoreInfiniteScrollCommand), typeof(ICommand), typeof(InfiniteListView), default(ICommand));
#endregion
#region Properties
/// <summary>
/// Gets or sets the property value that defines whether the list has reached the
/// end of the items source thereby declaring its not possible to load more itmes...
/// </summary>
public bool IsLoadMoreItemsPossible
{
get { return (bool)GetValue(IsLoadMoreItemsPossibleProperty); }
set
{
SetValue(IsLoadMoreItemsPossibleProperty, value);
}
}
/// <summary>
/// Gets or sets the property value that defines whether listview is busy loading
/// items in the background or not
/// </summary>
public bool IsBusy
{
get { return (bool)GetValue(IsBusyProperty); }
set
{
SetValue(IsBusyProperty, value);
// Setting the value to ActivityIndicator
if (ActivityIndicator_LoadingContents != null)
{
ActivityIndicator_LoadingContents.IsVisible = value;
}
}
}
#endregion
#region Commands
/// <summary>
/// <summary>
/// Gets or sets the command binding that is called whenever the listview is reaching the bottom items area
/// </summary>
public ICommand LoadMoreInfiniteScrollCommand
{
get { return (ICommand)GetValue(LoadMoreInfiniteScrollCommandProperty); }
set { SetValue(LoadMoreInfiniteScrollCommandProperty, value); }
}
#endregion
}
}
Как видите, свойство IsBusy должно отключить отображение ActivityIndicator после того, как больше нет сообщений.
Вот мой PostDataViewModel, привязка к ObservableCollection и PostTitle работает:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Newtonsoft.Json.Linq;
using Xamarin.Forms;
using SOD_APP_V2.Model;
namespace SOD_APP_V2.ViewModel
{
public class PostDataViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
//Bindable properties
public ObservableCollection<PostDataModel> PostDataCollection { get; set; }
public ICommand LoadMorePostsCommand { get; private set; }
public ICommand RejectPostCommand { get; private set; }
public static JObject SocialmediaSites { get; set; }
public object SelectedItem { get; set; }
bool isLoadMoreEnabled;
public bool IsLoadMoreEnabled
{
get
{
return isLoadMoreEnabled;
}
set
{
if (isLoadMoreEnabled != value)
{
isLoadMoreEnabled = value;
OnPropertyChanged((nameof(IsLoadMoreEnabled)));
}
}
}
bool isBusy;
public bool IsBusy
{
get
{
return isBusy;
}
set
{
if (isBusy != value)
{
isBusy = value;
OnPropertyChanged((nameof(IsBusy)));
}
}
}
string pageTitle;
public string PageTitle
{
get
{
return pageTitle;
}
set
{
if (pageTitle != value)
{
pageTitle = value;
OnPropertyChanged(nameof(PageTitle));
}
}
}
int currentPage = 1;
public int CurrentPage
{
get
{
return currentPage;
}
set
{
if(currentPage != value)
{
currentPage = value;
OnPropertyChanged(nameof(CurrentPage));
}
}
}
string primaryColor;
public string PrimaryColor
{
get
{
return primaryColor;
}
set
{
if (primaryColor != value)
{
primaryColor = value;
OnPropertyChanged(nameof(PrimaryColor));
}
}
}
string secondaryColor;
public string SecondaryColor
{
get
{
return secondaryColor;
}
set
{
if (secondaryColor != value)
{
secondaryColor = value;
OnPropertyChanged(nameof(SecondaryColor));
}
}
}
public PostDataViewModel()
{
PostDataCollection = new ObservableCollection<PostDataModel>();
SocialmediaSites = default(JObject);
PrimaryColor = ConfigController.GetPrimaryColor(ApiController.DeploymentDomain);
SecondaryColor = ConfigController.GetSecondaryColor(ApiController.DeploymentDomain);
IsBusy = true;
IsLoadMoreEnabled = true;
LoadMorePostsCommand =
new Command(async () => await GetPosts(), () => IsLoadMoreEnabled);
string deployment = ConfigController.GetDeploymentName(ApiController.DeploymentDomain);
MessagingCenter.Subscribe<PostsPage, JArray>(this, "translations", (sender, arg) => {
PageTitle = (arg[0].ToString() != "") ? arg[0].ToString() : "Posts from " + deployment;
});
if (deployment != null)
{
//TODO: lang packs
PageTitle = "Posts from " + deployment;
}
}
public async Task<bool> GetPosts()
{
JObject posts = await ApiController.GetPostsFromVendor<JObject>(CurrentPage.ToString());
if (posts != null)
{
IsLoadMoreEnabled = true;
foreach (var entry in posts)
{
//Building PostDataCollection here, binds to PostsPage as expected
}
CurrentPage += 1;
IsBusy = false; // Should hide indicator, but doesn't
return await Task.Run(() =>
{
Task.Delay(1);
return true;
});
} else {
IsLoadMoreEnabled = false; //No more posts to load, does not change the binding, this method still gets called then you scroll list view
return await Task.Run(() =>
{
Task.Delay(1);
return false;
});
}
}
protected virtual void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(nameof(propertyName)));
}
}
}
}
А вот PostsPage.XAML, где я связываю свою модель представления:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SOD_APP_V2"
xmlns:view="clr-namespace:SOD_APP_V2.View;assembly=SOD_APP_V2"
xmlns:viewModel="clr-namespace:SOD_APP_V2.ViewModel;assembly=SOD_APP_V2"
xmlns:controls="clr-namespace:SOD_APP_V2.Controls;assembly=SOD_APP_V2"
x:Class="SOD_APP_V2.PostsPage" x:Name="PostsPage" BackgroundColor="White" Padding="0"
>
<ContentPage.BindingContext>
<viewModel:PostDataViewModel x:Name="postDataViewModel"/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout Spacing="0" Padding="0" Margin="0" VerticalOptions="FillAndExpand">
<StackLayout Orientation="Vertical" Padding="10, 5, 10, 5" VerticalOptions="FillAndExpand">
<Label x:Name="titleLabel" Text="{Binding PageTitle}"
VerticalOptions="Start"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
BackgroundColor="Transparent"
HorizontalOptions="CenterAndExpand" />
<controls:InfiniteListView x:Name="listView"
SelectedItem="{Binding SelectedItem,Mode=TwoWay}"
IsLoadMoreItemsPossible="{Binding Path=BindingContext.IsLoadMoreEnabled, Source={x:Reference listView}}"
LoadMoreInfiniteScrollCommand="{Binding LoadMorePostsCommand}"
IsEnabled="true"
IsBusy="{Binding IsBusy, Mode=TwoWay}"
HasUnevenRows="true"
ItemsSource="{Binding PostDataCollection}"
SeparatorVisibility="None"
VerticalOptions="FillAndExpand">
<controls:InfiniteListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView>
<ContentView.Content>
<Frame HasShadow="false" CornerRadius="5" IsClippedToBounds="true" OutlineColor="{Binding Path=BindingContext.PrimaryColor, Source={x:Reference listView}}" Padding="0" Margin="10">
<!-- Grid lives here with posts, bindings to post model works with no problems-->
</Frame>
</ContentView.Content>
</ContentView>
</ViewCell>
</DataTemplate>
</controls:InfiniteListView.ItemTemplate>
</controls:InfiniteListView>
</StackLayout>
<StackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="End">
<Label x:Name="infoLabel" Text="test"
Opacity="0"
TextColor="White"
BackgroundColor="#337ab7"
HorizontalTextAlignment="Center">
</Label>
</StackLayout>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Итак, завершение
<controls:InfiniteListView x:Name="listView"
SelectedItem="{Binding SelectedItem,Mode=TwoWay}"
IsLoadMoreItemsPossible="{Binding
IsLoadMoreEnabled}"
LoadMoreInfiniteScrollCommand="{Binding
LoadMorePostsCommand}"
IsEnabled="true"
IsBusy="{Binding IsBusy, Mode=TwoWay}"
HasUnevenRows="true"
ItemsSource="{Binding PostDataCollection}"
SeparatorVisibility="None"
VerticalOptions="FillAndExpand">
Привязка PostDataColection работает, но IsBusy и IsLoadMoreItemsPossible не пытались изменить на Path = BindingContext.IsLoadMorePossible, по-прежнему безрезультатно.
ОБНОВЛЕНИЕ: я нашел решение, которое, кажется, работает, но это очень некрасиво. Если я использую MessagingCenter в своей ViewModel и вместо того, чтобы устанавливать для привязываемого свойства значение false, я отправляю сообщение в PostsPage и устанавливаю значение false в событии подписки, но это не похоже на правильное решение.