I'm trying to do some basic data binding from my XAML view to my view model in my Xamarin Forms (4.2) app. I navigate to the PhotoUploadPage from a different page.
PhotoUploadPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="AgentConnectMobile.Views.PhotoUploadPage"
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"
Title="Upload Photo"
mc:Ignorable="d">
<ContentPage.Content>
<!--Upload.Id below isn't showing -->
<Label Text="{Binding Upload.Id}" />
</ContentPage.Content>
</ContentPage>
PhotoUploadPage.xaml.cs
public partial class PhotoUploadPage : ContentPage
{
PhotoUploadViewModel viewModel;
public PhotoUploadPage(PhotoUploadViewModel viewModel)
{
this.viewModel = viewModel;
BindingContext = this.viewModel;
InitializeComponent();
}
}
PhotoUploadViewModel.cs
public class PhotoUploadViewModel : BaseViewModel
{
public Upload Upload { get; set; }
public PhotoUploadViewModel(Upload upload = null)
{
Upload = upload;
}
}
Upload.cs
public class Upload
{
public string Id;
}
Navigate to PhotoUploadPage
var upload = new Upload
{
Id = "abc123",
};
await Navigation.PushAsync(new PhotoUploadPage(new PhotoUploadViewModel(upload)));
I put breakpoints in PhotoUploadPage.xaml.cs and the BindingContext is getting set and I can see Upload on it with my Id value, but the value never appears in the Label text. I have also switched the order of InitializeComponent() and setting the BindingContext but that didn't solve anything either.
What am I doing wrong? I believe I'm doing this other places and it is working just fine...
Related
I have:
public class ViewCustomerViewModel
{
public Customer CustomerInfo { get; set; }
public static string baseUrl = "https://xxxx/Customers/";
public ViewCustomerViewModel()
{
CheckUserinfo();
}
public async void CheckUserinfo()
{
.....
var url = baseUrl;
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", xxx);
string jsonStr = await client.GetStringAsync(url);
var res = JsonConvert.DeserializeObject<Customer>(jsonStr);
CustomerInfo = res;
}
}
CustomerInfo returns oke
I have PageOne.xaml
<ContentPage ...>
<ContentPage.BindingContext>
<locals:ViewCustomerViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<Label Text={Binding xxxx} />
</ContentPage.Content>
</ContentPage>
PageOne.xaml.cs
This is how I get the data to Binding:
public Customer CustomerInfo { get; set; }
public PageOne()
{
InitializeComponent();
BindingContext = new ViewCustomerViewModel();
}
However: in PageOne.xaml page when I <Label Text={Binding xxxx} /> ---> Binding xxxx, I get no value of Customer class.
I was doing something wrong. Please help me how can I display CustomerInfo results. Thank you
Update
PageOne.xaml
<ContentPage ...>
<ContentPage.Content>
<Label Text={Binding xxxx} />
</ContentPage.Content>
</ContentPage>
The problem is that when the constructor completes, the instance is still being asynchronously initialized, and there isn’t an obvious way to determine when the asynchronous initialization has completed.
Instead of calling from the constructor, do it from the page's OnAppearing method, which you can make async.
Or you could change your viewmodel,make a Factory Pattern for it.From the link,there are several methods you could refer to.
I learnt from this tutorial and tried to build my own time tracker app using master detail page instead. But I met an issue. Here is a similar quick demo for the issue:
For MasterDetailPageDetail.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage 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"
xmlns:local="clr-namespace:QuickDemo"
x:Class="QuickDemo.MasterDetailPageDetail"
Title="Detail">
<TabbedPage.Children>
<NavigationPage Title="Tab One">
<x:Arguments>
<local:FirstPage BindingContext="{Binding FirstPageModel}" />
</x:Arguments>
</NavigationPage>
<NavigationPage Title="Tab Two">
</NavigationPage>
</TabbedPage.Children>
</TabbedPage>
For FirstPage.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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
NavigationPage.HasNavigationBar="False"
xmlns:buttons="clr-namespace:QuickDemo.Views.Buttons"
x:Class="QuickDemo.FirstPage">
<ContentPage.Content>
<StackLayout>
<StackLayout Padding="20,20,20,5">
<Label Text="{Binding CurrentStartDate, StringFormat='{0:h:mm tt}'}"
FontSize="20"
TextColor="Black"/>
<Label Text="{Binding RunningTotal, StringFormat='{}{0:h\\:mm\\:ss}'}"
FontSize="50" HorizontalTextAlignment="Center"
TextColor="Black"/>
</StackLayout>
</StackLayout>
</ContentPage.Content>
</ContentPage>
For DetailPageModel.cs:
public class DetailPageModel : PageModelBase
{
private FirstPageModel _firstPageModel;
public FirstPageModel FirstPageModel
{
get => _firstPageModel;
set => SetProperty(ref _firstPageModel, value);
}
public DetailPageModel(
FirstPageModel firstPageModel)
{
FirstPageModel = firstPageModel;
}
public override Task InitializeAsync(object navigationData = null)
{
return Task.WhenAny(base.InitializeAsync(navigationData),
FirstPageModel.InitializeAsync(null));
}
}
For FirstPageModel.cs:
public class FirstPageModel : PageModelBase
{
private DateTime _currentStartDate;
public DateTime CurrentStartDate
{
get => _currentStartDate;
set => SetProperty(ref _currentStartDate, value);
}
private TimeSpan _runningTotal;
public TimeSpan RunningTotal
{
get => _runningTotal;
set => SetProperty(ref _runningTotal, value);
}
private Timer _timer;
public FirstPageModel()
{
this.InitializeTimer();
}
private void InitializeTimer()
{
_timer = new Timer();
_timer.Interval = 1000;
_timer.Enabled = false;
_timer.Elapsed += TimerElapsed;
}
private void TimerElapsed(object sender, ElapsedEventArgs e)
{
RunningTotal += TimeSpan.FromSeconds(1);
}
public override async Task InitializeAsync(object navigationData)
{
CurrentStartDate = DateTime.Now;
RunningTotal = new TimeSpan();
await base.InitializeAsync(navigationData);
}
}
And I registered the page and pagemodel in the PageModelLocator.cs:
public class PageModelLocator
{
static TinyIoCContainer _container;
static Dictionary<Type, Type> _lookupTable;
static PageModelLocator()
{
_container = new TinyIoCContainer();
_lookupTable = new Dictionary<Type, Type>();
// Register pages and page models
Register<DetailPageModel, MasterDetailPageDetail>();
Register<FirstPageModel, FirstPage>();
// Register services (services are registered as singletons default)
}
public static T Resolve<T>() where T : class
{
return _container.Resolve<T>();
}
public static Page CreatePageFor(Type pageModelType)
{
var pageType = _lookupTable[pageModelType];
var page = (Page)Activator.CreateInstance(pageType);
var pageModel = _container.Resolve(pageModelType);
page.BindingContext = pageModel;
return page;
}
static void Register<TPageModel, TPage>() where TPageModel : PageModelBase where TPage : Page
{
_lookupTable.Add(typeof(TPageModel), typeof(TPage));
_container.Register<TPageModel>();
}
}
PageModelBase.cs and ExtendedBindableObject.cs are same as the tutorial. When I ran the emulator, I got this result:
I thought there would be a DateTime string and a zero time span. I feel like the data in FirstPageModel isn't initialized at all. I also tried to set CurrentStartTime in the constructor. Got the same result.
Did I miss something to display CurrentStartDate and RunningTotal on FirstPage? Any help or hints would be appreciated. Thanks in advance.
I believe your MasterDetailPageDetail is not being initialized (or BindingContext set for that matter). The architecture in the tutorial sets the BindingContext during a navigation event. Since you are setting the Master and Detail explicitly in the XAML of your MasterDetailPage, the Binding Context is not explicitly set and the InitializeAsync Method is not being called.
Add initialization in the Navigation Service
update NavigationService's NavigateToAsync method with the following, right after if (setRoot) and before if (page is TabbedPage tabbedPage):
if (page is MasterDetailPage mdp)
{
App.Current.MainPage = mdp;
// We need to initialize both Master's BindingContext as
// well as Detail's BindingContext if they are PageModelBases
if (mdp.Master.BindingContext is PageModelBase masterPM)
{
await masterPM.InitializeAsync(null);
}
if (mdp.Detail.BindingContext is PageModelBase detailPM)
{
await detailPM.InitializeAsync(null);
}
}
else if (page is TabbedPage tabbedPage)
// .... existing code here
Make sure you're setting the Binding Context for the Pages (Master and Detail). You can do this in the XAML or by resolving in the MasterDetailPage and PageModel like you did for the Tabbed Page
I can make a video to cover this if needed, and add it to the Series. Hope this helps!
Cheers,
Patrick
I am confused with how Xamarin binding works.
OneWay
Indicates that the binding should only propagate changes from source
(usually the View Model) to target (the BindableObject). This is the
default mode for most BindableProperty values.
So by default, if the values are set in the view model it will be reflected in the xaml pages.
But in the Xamarin default template, below is the code to insert a new item. Page doesn't have any two way binding mode set in the markup.
<ContentPage 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="Christianity.Mobile.Views.NewItemPage"
Title="New Item">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Cancel" Clicked="Cancel_Clicked" />
<ToolbarItem Text="Save" Clicked="Save_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Spacing="20" Padding="15">
<Label Text="Text" FontSize="Medium" />
<Entry Text="{Binding Item.Text}" d:Text="Item name" FontSize="Small" />
<Label Text="Description" FontSize="Medium" />
<Editor Text="{Binding Item.Description}" d:Text="Item description" FontSize="Small" Margin="0" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
Here I can see that default values of item are populated when a new page is created and also, edited name and description are available while saving the item.
My question - is two way binding implemented by default without having any binding mode set?
public partial class NewItemPage : ContentPage
{
public Item Item { get; set; }
public NewItemPage()
{
InitializeComponent();
Item = new Item
{
Text = "Item name",
Description = "This is an item description."
};
BindingContext = this;
}
async void Save_Clicked(object sender, EventArgs e)
{
MessagingCenter.Send(this, "AddItem", Item);
await Navigation.PopModalAsync();
}
async void Cancel_Clicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}
}
UPDATE
Here is my code to load data asynchronously
<?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:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Christianity.Mobile.Views.ItemDetailPage"
Title="{Binding Title}">
<StackLayout Spacing="20" Padding="15">
<Label Text="Text:" FontSize="Medium" />
<Label Text="{Binding Item.Title}" d:Text="Item title" FontSize="Small"/>
<Label Text="Description:" FontSize="Medium" />
<Label Text="{Binding Item.Description}" d:Text="Item description" FontSize="Small"/>
</StackLayout>
</ContentPage>
public class ItemDetailViewModel : BaseViewModel
{
public ItemListItemDTO SelectedItem { get; set; }
public ItemDTO Item { get; set; }
public ICommand LoadItemCommand;
public ItemDetailViewModel(IPageService pageService, ItemListItemDTO selectedItem)
{
SelectedItem = selectedItem;
LoadItemCommand = new Command(async () => await LoadItem());
}
public async Task LoadItem()
{
IsBusy = true;
try
{
// Both are not working
Item = await ItemsDataStore.GetItemAsync(SelectedItem.Id);
//await Device.InvokeOnMainThreadAsync(async () =>
//{
// Item = await ItemsDataStore.GetItemAsync(SelectedItem.Id);
//});
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
According to Xamarin.Forms Binding Mode, you can see that most bindable properties have a default binding mode of OneWay but the following properties have a default binding mode of TwoWay:
Date property of DatePicker
Text property of Editor, Entry, SearchBar, and EntryCell
IsRefreshing property of ListView
SelectedItem property of MultiPage
SelectedIndex and SelectedItem properties of Picker
Value property of Slider and Stepper
IsToggled property of Switch
On property of SwitchCell
Time property of TimePicker
These particular properties are defined as TwoWay for a very good reason:
When data bindings are used with the Model-View-ViewModel (MVVM) application architecture, the ViewModel class is the data-binding source, and the View, which consists of views such as Slider, are data-binding targets. MVVM bindings resemble the Reverse Binding sample more than the bindings in the previous samples. It is very likely that you want each view on the page to be initialized with the value of the corresponding property in the ViewModel, but changes in the view should also affect the ViewModel property.
The properties with default binding modes of TwoWay are those properties most likely to be used in MVVM scenarios.
Update:
For example, you get data using Web Api, then loading into ItemDTO Item, please comfirm that you have implement INotifyPropertyChanged interface for ItemDTO class to notify data changed.
public class ItemDTO:ViewModelBase
{
private string _Text;
public string Text
{
get { return _Text; }
set
{
_Text = value;
RaisePropertyChanged("Text");
}
}
private string _Description;
public string Description
{
get
{ return _Description; }
set
{
_Description = value;
RaisePropertyChanged("Description");
}
}
}
The ViewModelBase is the class that implement INotifyPropertyChanged.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I've got a very basic view.
<ContentPage x:Class="ThetaRex.InvestmentManager.Merlin.Views.ScenarioSelectionPage"
Title="{Binding Title}"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns="http://xamarin.com/schemas/2014/forms">
<StackLayout>
<ListView ItemsSource="{Binding Items}"/>
</StackLayout>
<ContentPage/>
The code behind is also very simple:
namespace ThetaRex.InvestmentManager.Merlin.Views
{
using System.ComponentModel;
using ThetaRex.InvestmentManager.Merlin.ViewModels;
using Xamarin.Forms;
public partial class ScenarioSelectionPage : ContentPage
{
public ScenarioSelectionPage()
{
InitializeComponent();
this.BindingContext = this.ViewModel = new ScenarioSelectionViewModel();
}
public ScenarioSelectionViewModel ViewModel { get; set; }
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.LoadItemsCommand.Execute(null);
}
}
}
Coming from a pure MVVM environment in WPF and UWP, I want to bind the view to the viewmodel in XAML, not using the this.Binding = ViewModel in the code behind. I've tried:
<ContentPage x:Class="ThetaRex.InvestmentManager.Merlin.Views.ScenarioSelectionPage"
xmlns:controls="clr-namespace:ThetaRex.InvestmentManager.Merlin.Controls"
BindingContext="{Binding ViewModel}"
Title="{Binding Title}"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns="http://xamarin.com/schemas/2014/forms">
But it didn't work. How do I bind to the ViewModel from XAML?
Note: I know that I can create a view model from scratch in XAML, but it doesn't use the same instance that the code behind in the view uses, so that's not an option.
If I understood what you want, the solution is build a ViewModelLocator like this:
ViewModelLocalizator Class
public static class ViewModelLocalizator
{
public static readonly BindableProperty AutoWireViewModelProperty =
BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocalizator), default(bool), propertyChanged: OnAutoWireViewModelChanged);
public static bool GetAutoWireViewModel(BindableObject bindable)
{
return (bool)bindable.GetValue(AutoWireViewModelProperty);
}
public static void SetAutoWireViewModel(BindableObject bindable, bool value)
{
bindable.SetValue(AutoWireViewModelProperty, value);
}
/// <summary>
/// VERIFY THE VIEW NAME AND ASSOCIATE IT WITH THE VIEW MODEL OF THE SAME NAME. REPLACING THE 'View' suffix WITH THE 'ViewModel'
/// </summary>
private static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue)
{
if (!(bindable is Element view))
{
return;
}
var viewType = view.GetType();
var viewModelName = viewType.FullName.Replace(".Views.", ".ViewModels.").Replace("Page", "ViewModel");
var viewModelType = Type.GetType(viewModelName);
if (viewModelType == null) { return; }
var vmInstance = Activator.CreateInstance(viewModelType);
if (vmInstance != null)
{
view.BindingContext = vmInstance;
}
}
}
Using It on your View
<ContentPage x:Class="YourProject.Views.YourTestPage"
Title="{Binding Title}"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:viewModelBase="clr-namespace:YourProject.ViewModels;assembly=YouProject"
viewModelBase:ViewModelLocalizator.AutoWireViewModel="true"
>
<StackLayout>
<ListView ItemsSource="{Binding Items}"/>
</StackLayout>
<ContentPage/>
I try to create something like that:
<Label Text="{Binding oResult.hi, StringFormat='Hallo: {0}'}" />
And it works fine! But i wish that the String "Hallo" should get out from the resx file.
Like this:
<Entry Placeholder="{i18n:TranslateExtension Text=password}" IsPassword="true" />
Also i will do a combination of both.
Thank you!
You could achieve this with a simple markup extension and strings in resx files.
Xaml extension:
using System;
using Xamarin.Forms.Xaml;
using Xamarin.Forms;
using System.Resources;
namespace i18n
{
[ContentProperty("Key")]
public class TranslateExtension : IMarkupExtension
{
public string Key { get; set; }
static ResourceManager ResourceManagerInstance;
#region IMarkupExtension implementation
public static void Init(ResourceManager r){
ResourceManagerInstance = r;
}
public object ProvideValue(IServiceProvider serviceProvider)
{
if (ResourceManagerInstance == null)
{
throw new InvalidOperationException("Call TranslateExtension.Init(ResourceManager) in your App.cs");
}
return ResourceManagerInstance.GetString(this.Key);
}
#endregion
}
}
Example App.cs:
using Xamarin.Forms;
using i18n;
namespace ResourceLocalizationMarkup
{
public class App : Application
{
public App()
{
TranslateExtension.Init(Localization.Strings.ResourceManager);
MainPage = new NavigationPage(new MyPage());
}
}
}
Example 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:i18n="clr-namespace:i18n"
x:Class="ResourceLocalizationMarkup.MyPage">
<ContentPage.Content>
<StackLayout Orientation="Vertical">
<Label Text="{i18n:TranslateExtension hello_world}"/>
<Label Text="{Binding Name, StringFormat={i18n:TranslateExtension thanks_user}}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>