I have a resuable control like this to display a loading spinner:
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Framework.Controls.Loading" x:Name="LoadingControl" IsVisible="{Binding LoadingIndicator}"
HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
<ContentView.Content>
<ActivityIndicator HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" Color="DarkBlue"
IsVisible="{Binding LoadingIndicator}"
IsRunning="{Binding LoadingIndicator}">
</ActivityIndicator>
</ContentView.Content>
</ContentView>
I am trying to consume it on a page like this:
<controls:Loading LoadingIndicator="{Binding IsLoading}"></controls:Loading>
However, the loading spinner fails to appear on-screen.
When I set the LoadingIndicator property to true, it appears just fine:
<controls:Loading LoadingIndicator="true"></controls:Loading>
My 'IsLoading' binding is definitely working properly, because if I place the following code directly in my XAML page it also works fine:
<ActivityIndicator HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"
Color="DarkBlue" IsVisible="{Binding IsLoading}" IsRunning="{Binding IsLoading}">
</ActivityIndicator>
Therefore, what is it about this that's wrong?
<controls:Loading LoadingIndicator="{Binding IsLoading}"></controls:Loading>
The 'IsLoading' property gets set on each of my pages from my view model. Here is a snippet from the view model:
public ICommand OnSave => new Command(async () =>
{
IsLoading = true;
await CreateItem();
IsLoading = false;
});
The code-behind for my control looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Loading : ContentView
{
public static readonly BindableProperty LoadingIndicatorProperty =
BindableProperty.Create(
propertyName: nameof(LoadingIndicator), typeof(bool),
typeof(Loading), default(string), BindingMode.OneWayToSource);
public bool LoadingIndicator
{
get => (bool)GetValue(LoadingIndicatorProperty);
set => SetValue(LoadingIndicatorProperty, value);
}
public Loading()
{
InitializeComponent();
BindingContext = this;
}
}
Do I need to write code to handle the change if the IsLoading binding gets updated?
This is the full code for the page where I am using the control:
ItemCreatePage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage Title="{Binding PageTitle}"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:userControls="clr-namespace:Framework.UserControls"
xmlns:converters="clr-namespace:Framework.ValueConverters"
xmlns:controls="clr-namespace:Framework.Controls;assembly=Framework.Android"
x:Class="Framework.Views.Item.ItemCreatePage">
<ContentPage.Resources>
<ResourceDictionary>
<converters:DoubleConverter x:Key="DoubleConverter"></converters:DoubleConverter>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<Grid>
<ScrollView>
<Grid RowSpacing="0" VerticalOptions="Start">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackLayout Grid.Row="1" Padding="20,20,20,0" VerticalOptions="Start">
<Label Text="Category" />
<userControls:BindablePicker
ItemsSource="{Binding Categories}"
SelectedItem="{Binding Path=Item.CategoryName, Mode=OneWay}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding Path=Item.CategoryId, Mode=TwoWay}"/>
<Label Text="Description" />
<Editor Text="{Binding Item.Description}" HeightRequest="100"/>
<Label Text="Area"/>
<Entry Text="{Binding Item.LineNumber}"/>
<Label Text="Identifier"/>
<Entry Text="{Binding Item.Identifier}"/>
<Label Text="Code"/>
<Entry Text="{Binding Item.Code}"/>
<Label Text="Priority" />
<userControls:BindablePicker
ItemsSource="{Binding Priorities}"
SelectedItem="{Binding Path=Item.ItemPriority, Mode=OneWay}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding Path=Item.ItemPriorityCode, Mode=TwoWay}"/>
<Label Text="Owner" />
<userControls:BindablePicker
ItemsSource="{Binding Users}"
SelectedItem="{Binding Path=Item.OwnerName, Mode=OneWay}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding Path=Item.OwnerId, Mode=TwoWay}"/>
<Label Text="Due Date" />
<DatePicker Date="{Binding Item.DateDue}" />
<Label Text="Date Identified" />
<DatePicker Date="{Binding Item.DateIdentified}" />
<Label Text="Status" />
<userControls:BindablePicker
ItemsSource="{Binding Statuses}"
SelectedItem="{Binding Path=Item.Status, Mode=OneWay}"
DisplayMemberPath="Name"
SelectedValuePath="Id"
SelectedValue="{Binding Path=Item.StatusCode, Mode=TwoWay}"/>
<Label Text="Comment" />
<Editor Text="{Binding Item.Comment}" HeightRequest="100"/>
<Label Text="IOM" />
<Entry Text="{Binding Item.OutcomeMeasurementInitial, Mode=TwoWay, Converter={StaticResource DoubleConverter}}" Keyboard="Numeric" />
<Label Text="FOM" />
<Entry Text="{Binding Item.OutcomeMeasurementFinal, Mode=TwoWay, Converter={StaticResource DoubleConverter}}" Keyboard="Numeric" />
<Label Text="Longitude" />
<Entry Text="{Binding Item.Longitude, Mode=TwoWay, Converter={StaticResource DoubleConverter}}" Keyboard="Numeric" />
<Label Text="Latitude" />
<Entry Text="{Binding Item.Latitude, Mode=TwoWay, Converter={StaticResource DoubleConverter}}" Keyboard="Numeric" />
<Button Margin="0,20,0,20" Command="{Binding OnSave}" BackgroundColor="{StaticResource Primary}"
BorderRadius="2" Text="Save" VerticalOptions="End" TextColor="White" ></Button>
</StackLayout>
</Grid>
</ScrollView>
<controls:Loading LoadingIndicator="{Binding IsLoading}"></controls:Loading>
</Grid>
</ContentPage.Content>
</ContentPage>
ItemCreatePage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ItemCreatePage : ContentPage
{
public ItemCreatePage ()
{
InitializeComponent ();
}
protected override async void OnAppearing()
{
var vm = BindingContext as ItemCreateViewModel;
vm.Item = new Data.Entities.Item();
await vm?.GetDeviceLocation();
base.OnAppearing();
}
}
The view model code:
public class ItemCreateViewModel : FormViewModel<Data.Entities.Item>
{
public async Task GetDeviceLocation()
{
this.Item = await this.Item.AddDeviceLocation();
OnPropertyChanged(nameof(this.Item));
}
public ILookupService LookupService { get; set; }
public IItemService ItemService { get; set; }
#region selectLists
public List<EnumListItem<ItemPriority>> Priorities => EnumExtensions.ToEnumList<ItemPriority>();
public List<EnumListItem<ItemStatus>> Statuses => EnumExtensions.ToEnumList<ItemStatus>();
public string PageTitle => $"{PageTitles.ItemCreate}{this.OfflineStatus}";
public List<Data.Entities.User> Users => UserService.GetAll(this.Offline);
public List<Data.Entities.Lookup> Categories => LookupService.GetLookups(this.Offline, LookupTypeCode.ItemCategories);
#endregion
public Data.Entities.Item Item { get; set; }
public ICommand OnSave => new Command(async () =>
{
await Loading(CreateItem);
});
private async Task CreateItem()
{
// ... Save logic is here
}
FormViewModel:
public class FormViewModel<T> : BaseViewModel
{
public IValidator<T> Validator => Resolve<IValidator<T>>();
public bool IsLoading { get; set; }
/// <summary>
/// Render a loading spinner whilst we process a request
/// </summary>
/// <param name="method"></param>
/// <returns></returns>
public async Task Loading(Func<Task> method)
{
IsLoading = true;
await method.Invoke();
IsLoading = false;
}
}
BaseViewModel:
public class BaseViewModel : IViewModelBase
{
public BaseViewModel()
{
if (this.GetCurrentUserToken() != null && !UserService.IsActive())
{
SettingService.ClearToken();
Bootstrapper.MasterDetailPage.IsPresented = false;
Application.Current.MainPage = new LoginPage();
}
}
public T Resolve<T>() => AutofacBootstrapper.Container.Resolve<T>();
public IUserService UserService => Resolve<IUserService>();
public INavigator Navigator => AutofacBootstrapper.Navigator;
public IDisplayAlertFactory DisplayAlert { get; set; }
public INavigation MasterNavigation => Bootstrapper.MasterDetailPage?.Detail?.Navigation;
public bool Offline => SettingService.GetSetting<bool>(CacheProperties.Offline);
public string OfflineStatus => this.Offline ? " - Offline" : string.Empty;
public Token GetCurrentUserToken() => SettingService.GetToken() ?? null;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyname = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
}
You don't need to set your custom control's BindingContext here:
public Loading()
{
InitializeComponent();
BindingContext = this;//It's wrong!
//because the custom control's BindingContext will
//automatically be set to the BindingContext of
//the page where it's used which is what we usually want.
}
Here is a way to achieve what you want:
Your Custom Control's XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Framework.Controls.Loading" x:Name="LoadingControl"
HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
<ContentView.Content>
<ActivityIndicator x:Name="TheIndicator" HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand" Color="DarkBlue"/>
</ContentView.Content>
</ContentView>
And here is its code-behind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Loading : ContentView
{
public static readonly BindableProperty LoadingIndicatorProperty =
BindableProperty.Create(propertyName:nameof(LoadingIndicator),
returnType: typeof(bool), declaringType: typeof(Loading), defaultValue: default(bool),
defaultBindingMode:BindingMode.Default, propertyChanged:LoadingBindingChanged);
private static void LoadingBindingChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var view = (Loading)(bindable);
view.SetLoadingVisibility((bool)newvalue);
}
public Loading()
{
InitializeComponent();
IsVisible = false; // we do this because by default a view' IsVisible property is true
}
public bool LoadingIndicator
{
get => (bool)GetValue(LoadingIndicatorProperty);
set => SetValue(LoadingIndicatorProperty, value);
}
public void SetLoadingVisibility(bool show)
{
IsVisible = show;
TheIndicator.IsVisible = show;
TheIndicator.IsRunning = show;
}
}
You are not invoking PropertyChanged event when you change IsLoading property. If you want UI to refresh you need to invoke this event for the chosen property.
Change implementation of IsLoading property to:
private bool _isLoading;
public bool IsLoading
{
get=> _isLoading;
set
{
_isLoading=value;
OnPropertyChanged(nameof(IsLoading));
}
}
and it should work
Related
I have created a ContentView with a single Label (I plan to add more later).
PageHeadingView.xaml
<ContentView.Content>
<StackLayout Orientation="Vertical" BackgroundColor="Red">
<Label x:Name="HeadingLabel" Text="{Binding HeadingText}" />
</StackLayout>
</ContentView.Content>
I defined a BindableProperty in my code behind. I also set the BindingContext of my view to be itself.
PageHeadingView.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PageHeadingView : ContentView
{
public PageHeadingView()
{
InitializeComponent();
this.BindingContext = this;
}
public static readonly BindableProperty HeadingTextProperty = BindableProperty.Create(nameof(HeadingText), typeof(string), typeof(PageHeadingView), default(string));
public string HeadingText { get => (string)GetValue( HeadingTextProperty); set => SetValue(HeadingTextProperty, value); }
}
I then added the View to my ContentPage. I also added a test Label inside a StackLayout to ensure my bindings were working correctly.
HomePage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyProject.Views"
x:Class="MyProject.HomePage">
<ContentPage.Content>
<StackLayout Orientation="Vertical">
<views:PageHeadingView HeadingText="{Binding Name}" />
<Label Text="{Binding Name}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
And set my BindingContext in code.
HomePage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HomePage : ContentPage
{
public HomePage()
{
InitializeComponent();
//ViewModel contains a string property named: Name;
this.BindingContext = new ViewModel();
}
}
When I run my code, my PageHeadingView does not display any text. I can see the red background color, so I know the control has been added to the Page correctly. The test Label I placed in StackLayout also works correctly, and I am able to see the bound value.
What do I need to do to make my CustomView display Bindable content?
From your code, you may have some problems when using bindableproperty bidning, I create simple sample that you can take a look:
PageHeadingView.xaml:
<ContentView.Content>
<StackLayout BackgroundColor="Red" Orientation="Vertical">
<Label x:Name="HeadingLabel" />
</StackLayout>
</ContentView.Content>
PageHeadingView.cs, you can update HeadingText value by HeadingTextPropertyChanged, then display HeadingLabel.
public partial class PageHeadingView : ContentView
{
public static BindableProperty HeadingTextProperty= BindableProperty.Create(
propertyName: "HeadingText",
returnType: typeof(string),
declaringType: typeof(PageHeadingView),
defaultValue: "",
defaultBindingMode: BindingMode.OneWay,
propertyChanged: HeadingTextPropertyChanged);
public string HeadingText
{
get { return base.GetValue(HeadingTextProperty).ToString(); }
set { base.SetValue(HeadingTextProperty, value); }
}
private static void HeadingTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (PageHeadingView)bindable;
control.HeadingLabel.Text = newValue.ToString();
}
public PageHeadingView()
{
InitializeComponent();
}
}
<StackLayout>
<customcontrol:PageHeadingView HeadingText="{Binding Name}" />
<Label Text="{Binding Name}" />
</StackLayout>
You are binding the wrong element in PageHeadingView.
Option 1:
Add "Content" in PageHeadingView.xaml.cs
this.Content.BindingContext = this;
Option 2:
Remove "Content" in PageHeadingView.xaml
<!--<ContentView.Content>-->
<StackLayout Orientation="Vertical" BackgroundColor="Red">
<Label x:Name="HeadingLabel" Text="{Binding HeadingText}" />
</StackLayout>
<!--</ContentView.Content>-->
Edited as quoted the wrong class previously.
Trying to fire a command inside a stacklayout with itemssource. I wonder why the NavigateToProductListViewShopTappedCommand is not getting fired.
Tried multiple command approaches:
1)
Command="{Binding Source={RelativeSource AncestorType={x:Type local:MyShopsListViewModel}}, Path=NavigateToProductListViewShopTappedCommand}"
Command="{Binding BindingContext.NavigateToProductListViewShopTappedCommand, Source={x:Reference Page}}"
Command="{Binding Path=BindingContext.NavigateToProductListViewShopTappedCommand, Source={x:Reference Page}}"
None are not working
Code:
<?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:controls="clr-namespace:BoerPlaza.Controls.Shop"
xmlns:local="clr-namespace:BoerPlaza.ViewModels"
xmlns:behaviors="clr-namespace:BoerPlaza.Behaviors"
x:Class="BoerPlaza.Views.Shop.MyShopsPage"
x:Name="Page"
Title="Mijn winkels">
<ContentPage.Content>
<ScrollView>
<StackLayout Margin="{StaticResource margin-side-std}"
Padding="{StaticResource padding-top-bottom-std}"
BindableLayout.ItemsSource="{Binding Shops}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<controls:ShopCardTemplateView Shop="{Binding .}"
ControlTemplate="{StaticResource ShopCardTemplateView}">
<controls:ShopCardTemplateView.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:MyShopsListViewModel}}, Path=NavigateToProductListViewShopTappedCommand}"
CommandParameter="{Binding .}">
<!--Command="{Binding BindingContext.NavigateToProductListViewShopTappedCommand, Source={x:Reference Page}}"-->
</TapGestureRecognizer>
</controls:ShopCardTemplateView.GestureRecognizers>
</controls:ShopCardTemplateView>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
Code behind
public partial class MyShopsPage : ContentPage
{
private readonly MyShopsListViewModel _viewModel;
public MyShopsPage()
{
InitializeComponent();
BindingContext = _viewModel = new MyShopsListViewModel(App.ShopDataStore, App.DialogService);
_viewModel.LoadShopsOnUserIdCommand.Execute("B22698B8-42A2-4115-9631-1C2D1E2AC5F7");
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
}
ViewModel:
[QueryProperty(nameof(UserId), nameof(UserId))]
public class MyShopsListViewModel : BaseViewModel
{
private string _userId;
private ObservableCollection<ShopDbViewModel> _shops;
private readonly IShopDataStore _shopDataStore;
private readonly IDialogService _dialogService;
public ObservableCollection<ShopDbViewModel> Shops
{
get
{
return _shops;
}
set
{
_shops = value;
OnPropertyChanged(nameof(Shops));
}
}
public void OnAppearing()
{
IsBusy = true;
}
public ICommand LoadShopsOnUserIdCommand { get; set; }
public ICommand NavigateToProductListViewShopTappedCommand { get; set; }
public MyShopsListViewModel(IShopDataStore shopDataStore, IDialogService dialogService)
{
this._shopDataStore = shopDataStore;
this._dialogService = dialogService;
Shops = new ObservableCollection<ShopDbViewModel>();
LoadShopsOnUserIdCommand = new Command<string>(async (string userId) => await ExecuteLoadShopsOnUserId(userId));
NavigateToProductListViewShopTappedCommand = new Command<ShopDbViewModel>(async (ShopDbViewModel shop) => await ExecuteNavigateToProductListViewShopTappedCommandAsync(shop));
}
private async Task ExecuteNavigateToProductListViewShopTappedCommandAsync(ShopDbViewModel shop)
{
if (shop == null)
return;
await Shell.Current.GoToAsync($"{nameof(ProductsPage)}?{nameof(MyProductsListViewModel.ShopId)}={shop.Id}");
}
public string UserId
{
get
{
return _userId;
}
set
{
_userId = value;
LoadShopsOnUserIdCommand.Execute(value);
}
}
private async Task ExecuteLoadShopsOnUserId(string userId)
{
var current = Connectivity.NetworkAccess;
if (current == NetworkAccess.Internet)
{
try
{
Shops.Clear();
var shops = await _shopDataStore.GetShopOnUserIdAsync(userId);
foreach(var shop in shops)
{
Shops.Add(shop);
}
}
catch (Exception ex)
{
await _dialogService.ShowDialog(ex.Message, "An error has occurred", "OK");
}
finally
{
IsBusy = false;
}
}
else
{
await _dialogService.ShowDialog("No active internet connection", "Connection error", "OK");
IsBusy = false;
}
}
}
If you define the ICommand in the ViewModel directly , you could set the binding path like following
Command="{Binding Path=BindingContext.NavigateToProductListViewShopTappedCommand, Source={x:Reference Page}}"
I've found the problem
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ffimage="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:customcontrols="clr-namespace:BoerPlaza.Controls"
x:Class="BoerPlaza.Controls.Shop.ShopCardTemplateView">
<ContentView.Resources>
<ControlTemplate x:Key="ShopCardTemplateView">
<!-- Card Header -->
<!-- for displaying products and categories on homepage -->
<StackLayout Spacing="1"
HorizontalOptions="FillAndExpand"
Margin="{StaticResource margin-card}">
<!-- On click - shows the product detail view page -->
<StackLayout.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" />
</StackLayout.GestureRecognizers>
<!-- Image frame -->
<Frame BackgroundColor="{StaticResource image-box-color}"
CornerRadius="0"
HasShadow="False"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
HeightRequest="100">
<!-- Product Image -->
....
As you can see this is the control template I'm using for the MyShopsPage. This is a shop card. Inside this shop card I already had an StackLayout.GestureRecognizers. Somehow when I was clicking on the control template, I was actually clicking on this.
I always thought everything flows from top to bottom in events, but this seems different. Something that is on top on something else does not mean anything in xaml.
I need to set two ViewModels from the code behind in the xaml code. Or if there is better way doing would be great to.
When I do it like this way the application crashes. When I set ProductDetailViewModel in the code behind (BindingContext = ViewModel) everything works fine.
update
It's not an good idea to pass viewModels as parameters.
I have now one class "ViewModelLocator" which contains all the ViewModels as static properties. Use Google for more info. This way things are way easier.
example
ViewModelLocator
public static class ViewModelLocator
{
public static AddProductViewModel AddProductViewModel { get; set; } = new AddProductViewModel(App.ProductDataStore, App.NavigationService);
}
end update
update 2
As #Waescher stated, it's better to use FreshMvvm. The static approach is simple and fast but not good for slow devices or larger apps. Thanks.
end update 2
**Xamarin.Forms.Xaml.XamlParseException:** 'Position 9:10. Can not find the object referenced by `ProductDetailViewModel`'
Since I can't set the ViewModels directly in the xaml I need to do it by reference from code behind.
See < *** First ViewModel *** > and < *** Second ViewModel *** > in the xaml code.
<?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:controls="clr-namespace:BoerPlaza.Controls"
xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
x:Class="BoerPlaza.Views.Product.ProductCustomerPictures">
<ContentPage.BindingContext>
<x:Reference Name="ProductDetailViewModel" /><!-- *** First ViewModel ***!-->
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<!-- Total image count -->
<Label Text="{Binding Product.UserImages.Total}"
Style="{StaticResource H2}" />
<!-- Title -->
<Label Text="{Binding Product.Title}"
Style="{StaticResource H1}" />
<!-- reviews -->
<StackLayout Orientation="Horizontal">
<controls:StarDisplayTemplateView x:Name="customRattingBar"
SelectedStarValue="{Binding Product.RatingTotal}" />
<Label Text="{Binding Product.RatingAmount, StringFormat='{0} reviews | '}" />
<Label Text="Schrijf een review" />
</StackLayout>
<Label Text="{Binding Product.Title, StringFormat='Heb je een productfoto van {0} die je wilt delen? '}" />
<Button Text="Foto's toevoegen"
Command="{Binding SelectImagesCommand}"
BackgroundColor="{StaticResource neutral-color}"
BorderColor="{StaticResource alt-color}"
BorderWidth="1"
TextColor="{StaticResource primary-color}"
HorizontalOptions="Start"
HeightRequest="40"
FontSize="12" />
<!-- hr -->
<BoxView Style="{StaticResource separator}" />
<flv:FlowListView FlowColumnCount="3"
x:Name="listItems"
FlowItemsSource="{Binding Media}"
SeparatorVisibility="None"
HasUnevenRows="false"
RowHeight="100"
HeightRequest="0">
<flv:FlowListView.BindingContext>
<x:Reference Name="MultiMediaPickerViewModel" /> <!-- *** Second ViewModel ***!-->
</flv:FlowListView.BindingContext>
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Grid>
<ffimageloading:CachedImage DownsampleToViewSize="true"
HeightRequest="100"
Source="{Binding PreviewPath}"
Aspect="AspectFill"
HorizontalOptions="FillAndExpand">
</ffimageloading:CachedImage>
<Image Source="play"
IsVisible="false"
HorizontalOptions="End"
VerticalOptions="End">
<Image.Triggers>
<DataTrigger TargetType="Image"
Binding="{Binding Type}"
Value="Video">
<Setter Property="IsVisible"
Value="True" />
</DataTrigger>
</Image.Triggers>
</Image>
</Grid>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Code behind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ProductCustomerPictures : ContentPage
{
public ProductDetailViewModel ProductDetailViewModel
{
get { return _productDetailViewModel; }
set { _productDetailViewModel = value; }
}
public MultiMediaPickerViewModel MultiMediaPickerViewModel
{
get { return _multiMediaPickerViewModel; }
set { _multiMediaPickerViewModel = value; }
}
private ProductDetailViewModel _productDetailViewModel;
private MultiMediaPickerViewModel _multiMediaPickerViewModel;
public ProductCustomerPictures(ProductDetailViewModel viewModel)
{
InitializeComponent();
ProductDetailViewModel = viewModel;
MultiMediaPickerViewModel = new MultiMediaPickerViewModel(MultiMediaPickerServiceStaticVariableHolder.MultiMediaPickerService);
}
}
If I understood this correctly and if you want to keep the pattern to pass in the view model as constructor argument ...
public ProductCustomerPictures(ProductDetailViewModel viewModel)
{
InitializeComponent();
ProductDetailViewModel = viewModel;
MultiMediaPickerViewModel = new MultiMediaPickerViewModel(MultiMediaPickerServiceStaticVariableHolder.MultiMediaPickerService);
}
... then you can remove this completely ...
<ContentPage.BindingContext>
...
</ContentPage.BindingContext>
... and this property ...
public ProductDetailViewModel ProductDetailViewModel
{
get { return _productDetailViewModel; }
set { _productDetailViewModel = value; }
}
Instead, just set the BindingContext directly in the constructor.
public ProductCustomerPictures(ProductDetailViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel; // <-- here
MultiMediaPickerViewModel = new MultiMediaPickerViewModel(MultiMediaPickerServiceStaticVariableHolder.MultiMediaPickerService);
}
Now, each and every control in the XAML is binding to the ProductDetailViewModel.
But you still have the FlowListView which should bind to the MultiMediaPickerViewModel. Instead of setting its binding context directly in XAML, it is common to use the binding with a reference, but first you have to give the whole page a name with which we can refer in the binding:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
...
...
x:Name="thisPage" <--- here
x:Class="BoerPlaza.Views.Product.ProductCustomerPictures">
Now, you can use the name as reference in the binding expression:
<flv:FlowListView FlowColumnCount="3"
x:Name="listItems"
FlowItemsSource="{Binding Source={x:Reference thisPage}, Path=MultiMediaPickerViewModel.Media}"
SeparatorVisibility="None"
HasUnevenRows="false"
RowHeight="100"
HeightRequest="0">
"{Binding Source={x:Reference thisPage}, Path=MultiMediaPickerViewModel.Media}" uses the page itself (by name thisPage) and binds to the property Media of the property MultiMediaPickerViewModel of the page.
With that, you can safely remove this code as well:
<flv:FlowListView.BindingContext>
...
</flv:FlowListView.BindingContext>
By the way, you can condense the properties in the code behind:
public MultiMediaPickerViewModel MultiMediaPickerViewModel { get; private set; }
public ProductCustomerPictures(ProductDetailViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
MultiMediaPickerViewModel = new MultiMediaPickerViewModel(MultiMediaPickerServiceStaticVariableHolder.MultiMediaPickerService);
}
While trying to show data over form in Xamarin Forms app, a property in the model which is nullable DateTime is freezing the app. I know this is the offending property because if I remove Xaml for this binding, everything works fine.
While stepping through the code, the LoginTime property from source (fetched from db) and the getter from ViewModel's User property are going into infinite loop. I can't possibly imagine why.
Model:
public class User : PersistentEntity
{
private string _userId;
public string UserId
{
get => _userId;
set => SetProperty(ref _userId, value);
}
private string _userName;
public string UserName
{
get => _userName;
set => SetProperty(ref _userName, value);
}
private string _email;
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}
private DateTime? _loginTime;
public DateTime? LoginTime
{
get => _loginTime;
set => SetProperty(ref _loginTime, value);
}
}
ViewModel:
public class SettingsViewModel : ViewModelBase
{
private readonly SettingsSession _settingsSession;
private User _user;
public User User
{
get => _user;
set => SetProperty(ref _user, value);
}
public SettingsViewModel(INavigationService navigationService, SettingsSession settingsSession)
: base(navigationService)
{
Title = "Settings";
_settingsSession = settingsSession;
}
private DelegateCommand _fetchUserCommand;
public DelegateCommand FetchUserCommand =>
_fetchUserCommand ?? (_fetchUserCommand = new DelegateCommand(ExecuteFetchUserCommand, CanExecuteFetchUserCommand));
async void ExecuteFetchUserCommand()
{
User = await _settingsSession.FetchUser();
}
bool CanExecuteFetchUserCommand()
{
return true;
}
}
View:
<TableView Intent="Settings">
<TableRoot>
<TableSection Title="User">
<ViewCell>
<StackLayout Orientation="Horizontal"
Padding="2">
<Label Text="User Id"
WidthRequest="150"
Margin="5,0,0,0"
VerticalOptions="CenterAndExpand"/>
<Entry WidthRequest="250"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
Placeholder="UserId"
Text="{Binding User.UserId}"/>
</StackLayout>
</ViewCell>
<ViewCell>
<StackLayout Orientation="Horizontal"
Padding="2">
<Label Text="User Name"
WidthRequest="150"
Margin="5,0,0,0"
VerticalOptions="CenterAndExpand"/>
<Entry WidthRequest="250"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
Placeholder="User Name"
Text="{Binding User.UserName}"/>
</StackLayout>
</ViewCell>
<ViewCell>
<StackLayout Orientation="Horizontal"
Padding="2">
<Label Text="Login Time"
WidthRequest="150"
Margin="5,0,0,0"
VerticalOptions="CenterAndExpand"/>
<Entry WidthRequest="250"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
Placeholder="Login Time"
Text="{Binding User.LoginTime, StringFormat='{0:dd-MM-yyyy HH:mm}'}"/>
</StackLayout>
</ViewCell>
</TableSection>
</TableRoot>
</TableView>
Any help is appreciated.
I would like to bind an Entry with a Slider and vice versa. I wrote something like this:
<Entry x:Name="myEntry" Text="{Binding Value, Mode=TwoWay}" BindingContext="{x:Reference slider}"/>
<Slider x:Name="slider" Maximum="100" Minimum="0" BindingContext="{x:Reference myEntry}"/>
When I use the slider, the value in the entry is updated, but when I put manually some value in the Entry, the value append a 0 or change to 0. What can be the problem. I am working on android.
You should bind both your Slider and Entry to string/ integer in a backing View Model.
class MyViewModel
{
private int _sliderValue;
public string EntryText
{
get => _sliderValue.ToString();
set => SetProperty(ref _sliderValue, int.Parse(value) );
}
public int SliderValue
{
get => _sliderValue;
set => (ref _sliderValue, value);
}
}
And in the view
<Entry Text="{Binding EntryText}" />
<Slider Value="{Binding SliderValue}" />
More MVVM Info
Fresh MVVM for Xamarin
Caliburn Micro for Xamarin
please refer the following xaml code
<Frame HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Entry Text="{Binding Path=Value}"
FontSize="18"
x:Name="label"
BindingContext="{x:Reference Name=slider}"/>
<Slider x:Name="slider"
Maximum="1500"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</Frame>
The same can be achieved by using the view model please refer the code
Xaml
<Frame HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Entry x:Name="NameEntry" Placeholder="Enter Name" Text="{Binding Forename,Mode=TwoWay}" />
<Slider Value="{Binding Forename}" Minimum="0" Maximum="10"/>
</StackLayout>
</Frame>
------------------- now see the model in a c# file-----------------------------------
public partial class MainPage : ContentPage
{
public class DetailsViewModel : INotifyPropertyChanged
{
int forename;
public int Forename
{
get
{
return forename;
}
set
{
if (forename != value)
{
forename = value;
OnPropertyChanged ("Forename");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var changed = PropertyChanged;
if (changed != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public MainPage()
{
InitializeComponent();
BindingContext = new DetailsViewModel();
}
}