I have a sample windows phone 7 project where I test some MVVM stuff, however I came across a problem.
My code looks like this:
This is from my View which is a MainPage:
<Grid>
<ListBox x:Name="list" ItemsSource="{Binding _reviews}"/>
</Grid>
This is code behind for the View:
public MainPage()
{
this.Loaded += MainPage_Loaded;
// Line below makes list show what it is supposed to show
// list.ItemsSource = (DataContext as MainPageVM)._reviews;
DataContext = new MainPageVM();
InitializeComponent();
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// DataContext is set to the right object!
var obj = list.DataContext;
}
Code for ViewModel
class MainPageVM
{
public ObservableCollection<Review> _reviews { get; set; }
public MainPageVM()
{
_reviews = GetReviews();
}
private ObservableCollection<Review> GetReviews()
{
ObservableCollection<Review> reviews = new ObservableCollection<Review>();
reviews.Add(new Review() { User = "Lol", Text = "Cool", Country = "UK"});
reviews.Add(new Review() { User = "misterX", Text = "aWESCOM APP", Country = "USA"});
reviews.Add(new Review() { User = "meYou", Text = "The best", Country = "UK"});
return reviews;
}
And here is my model:
class Review
{
public string Text { get; set; }
public string User { get; set; }
public string Country { get; set; }
}
Could you please point out where is the error and why I am able to set the ItemSource in code behind, but not via binding in XAML
The problem is that your view model class does not implement the INotifyPropertyChanged interface and you are not raising the PropertyChanged event, so the view does not know that the property you are binding to has changed.
If you're not sure about how to implement this interface, take a look at this post on Silverlight Show.
UPDATE: For most properties the above is true, however, in this instance because it's an ObservableCollection it's not necessary. However, because your view model class isn't public the view can't bind to it. Do you see any binding errors in the Output window whilst debugging?
Related
In my Xamarin app, I'm using a ListView to display phone numbers as well as other data for vendors.
The view model I use for this page looks like this:
public class VendorProfileViewModel : BaseViewModel
{
public LayoutState _mainState;
ContactModel contact;
public VendorProfileViewModel()
{
Title = string.Empty;
IsBusy = true;
MainState = LayoutState.Loading;
}
public LayoutState MainState
{
get => _mainState;
set => SetProperty(ref _mainState, value);
}
public ContactModel Contact
{
get => contact;
set
{
if (contact == value)
return;
contact = value;
OnPropertyChanged();
}
}
public async void Init(Guid id)
{
Contact = await MyApi.GetContact(123);
IsBusy = false;
MainState = LayoutState.None;
}
}
And I have a public List<PhoneNumber> PhoneNumbers { get; set; } property in the ContactModel class.
In my XAML page, I'm trying to bind items to PhoneNumbers like this:
<ListView
ItemSource={Binding Contact.PhoneNumbers}>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text={Binding PhoneNumber} />
</DateTemplate>
</ListView.ItemTemplate>
</ListView>
I'm getting an error that states that the PhoneNumber property was not found in view model.
How do I bind to property of a subclass of my view model in a ListView?
I have a new Xamarin Forms 5 app and I'm having trouble with data binding.
First, I display a message that tells the user how many items are in his list. Initially, this is 0. It's displayed by DisplayMessage property of the view model.
Then, the Init() method gets called and once the API call is finished, there are some items in MyList. I put break points to make sure that the API call works and I end up with some data in MyList property.
Because I change the value of message in my Init() method, I was expecting the message to change and display the number of items in the list but it's not changing even though I have some items in MyList.
I created a new ViewModel that looks like this:
public class MyViewModel : BaseViewModel
{
public List<MyItem> MyList { get; set; } = new List<MyItem>();
string message = "You have no items in your list... ";
public string DisplayMessage
{
get => message;
set
{
if(message == value)
return;
message = value;
OnPropertyChanged();
}
}
public async void Init()
{
var data = await _myService.GetData();
if(data.Count > 0)
message = $"You have {data.Count} items in your list!";
MyList = data;
}
}
My MainPage code behind looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
MyViewModel _vm;
MainPage()
{
InitializeComponent();
_vm = new MyViewModel();
this.BindingContext = _vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.Init();
}
}
I didn't change anyting in the base view model, except I added my service and it looks like this:
public class BaseViewModel : INotifyPropertyChanged
{
public IMyApiService MyApi => DependencyService.Get<IMyApiService>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
I'd appreciatae someone telling me where my mistake is. Thanks.
Without seeing the Xaml, I can't 100% answer, but here are a couple of things I see:
You are setting the "message" through the field, not the property. Since you are setting the field directly the OnPropertyChanged event isn't firing so the UI isn't getting notified that the value has changed.
I am guessing you are binding "MyList" to some sort of CollectionView or something? If it's a readonly view, using a List is ok as the collection is never updated. However, if you plan on adding or removing items at runtime, it needs to be an "ObservableCollection" for the same reason as above, the UI isn't notified of new items in a List, but an ObservableCollection will notify the UI of changes to it, so it can update.
Is what Jason mentions above in his comment. The MyList property should be setup like the other properties with the OnPropertyChanged.
I implemented language changing trough AppResources.resx files, I have 2 of these: AppResources.resx and AppResources.fr.resx. Switching the language with the following code:
private void Language_switch(object sender, EventArgs e)
{
var lang_switch = Lang.Text;
if (lang_switch == "FR")
{
CultureInfo language = new CultureInfo("fr");
Thread.CurrentThread.CurrentUICulture = language;
AppResources.Culture = language;
}
else
{
CultureInfo language = new CultureInfo("");
Thread.CurrentThread.CurrentUICulture = language;
AppResources.Culture = language;
}
Application.Current.MainPage = new NavigationPage(new PointsPage());
}
The language switches fine but whenever I do, the app turns dark and the AppShell seems to break, it only shows the top bar with toolbar items i made (in what seems to be the standard xamarin color) and showing what seems to be it trying to show the navigation at the bottom, but this only looks like a bar but doesn't seem to have navigation on it and doesn't have any of the labels for navigation.
The content on the page also seems to overlap with this bar going over it if I scroll down. If I press the switch button again it does still switch languages but stay in this dark mode. I don't have any of the dark color set in my app and don't have a dark mode implemented.
It also seems to be doing this on every single page I do it on. How can i stop this from happening so it uses the layout i have made for my app and doesn't turn dark?
Edit: I found out that problem isn't in the language switch. When I go to another page just using
Application.Current.MainPage = new NavigationPage(new PointsPage());
and removing the the language switch code it still does the weird thing where it changes colors. From what it looks like to me is that the page gets put on top without the AppShell moving to be on top of that. Is there a way to reload the AppShell?
Edit2: I managed to fix it. As I suspected the AppShell wasn't reloading and wasn't being put on top of the reloaded page. I added
Application.Current.MainPage = new AppShell();
underneath the page reload and now everything is working
When you use the .resx file to make the localization, create the resx file with the matched file name, when you change the system language, reopen the app would show the matched resource your set in .resx.
For more details about it, you could refer to the MS docs. https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/localization/text?pivots=windows
Code sample: https://github.com/xamarin/xamarin-forms-samples/tree/main/UsingResxLocalization
If you want to change it at runtime, you could use ResourceManager. It provides convenient access to culture-specific resources at runtime.
I make a simple example for your reference.
MainPage: String1, Settings are the key in .resx file.
<StackLayout>
<Label Text="{Binding Resources[String1]}"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Text="{Binding Resources[Settings]}"
HorizontalOptions="Center"
Clicked="Button_Clicked" />
</StackLayout>
Code behind:
public MainPage()
{
InitializeComponent();
this.BindingContext = new MainPageViewModel();
}
private void Button_Clicked(object sender, EventArgs e)
{
Navigation.PushAsync(new SettingsPage());
}
SettingsPage:
<StackLayout>
<Label Text="{Binding Resources[PickLng]}" />
<Picker ItemsSource="{Binding Languages}" SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}" />
</StackLayout>
Code behind:
public SettingsPage()
{
InitializeComponent();
BindingContext = new SettingsViewModel();
}
ViewModel:
public class CultureChangedMessage
{
public CultureInfo NewCultureInfo { get; private set; }
public CultureChangedMessage(string lngName)
: this(new CultureInfo(lngName))
{ }
public CultureChangedMessage(CultureInfo newCultureInfo)
{
NewCultureInfo = newCultureInfo;
}
}
public class LocalizedResources : INotifyPropertyChanged
{
const string DEFAULT_LANGUAGE = "en";
readonly ResourceManager ResourceManager;
CultureInfo CurrentCultureInfo;
public string this[string key]
{
get
{
return ResourceManager.GetString(key, CurrentCultureInfo);
}
}
public LocalizedResources(Type resource, string language = null)
: this(resource, new CultureInfo(language ?? DEFAULT_LANGUAGE))
{ }
public LocalizedResources(Type resource, CultureInfo cultureInfo)
{
CurrentCultureInfo = cultureInfo;
ResourceManager = new ResourceManager(resource);
MessagingCenter.Subscribe<object, CultureChangedMessage>(this,
string.Empty, OnCultureChanged);
}
private void OnCultureChanged(object s, CultureChangedMessage ccm)
{
CurrentCultureInfo = ccm.NewCultureInfo;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item"));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ViewModelBase : INotifyPropertyChanged
{
public LocalizedResources Resources
{
get;
private set;
}
public ViewModelBase()
{
Resources = new LocalizedResources(typeof(AppResources), App.CurrentLanguage);
}
public void OnPropertyChanged([CallerMemberName] string property = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class MainPageViewModel : ViewModelBase
{ }
public class SettingsViewModel : ViewModelBase
{
public List<string> Languages { get; set; } = new List<string>()
{
"EN",
"FR"
};
private string _SelectedLanguage;
public string SelectedLanguage
{
get { return _SelectedLanguage; }
set
{
_SelectedLanguage = value;
SetLanguage();
}
}
public SettingsViewModel()
{
_SelectedLanguage = App.CurrentLanguage;
}
private void SetLanguage()
{
App.CurrentLanguage = SelectedLanguage;
MessagingCenter.Send<object, CultureChangedMessage>(this,
string.Empty, new CultureChangedMessage(SelectedLanguage));
}
}
I am using shell and I want tabs on all the pages. so I am following standard way of shell navigation but the problem is I don't know how to pass an object along with the navigation.
await Shell.Current.GoToAsync(page.ToString());
doing this for navigation
Routing.RegisterRoute("TesProject.Views.DetailView", typeof(DetailView));
Registering Route like this
I want to pass a complete object from my list view to detail view. How can I do that?
Xamarin.Forms Shell Navigation Hierarchy with parameters
I saw this but I don't think this will work in my case because I can't pass a complete model object like this.
I wrote a small example for your reference.
In the sending class, you can pass parameters like $"AferPage?param={param}".
Here is the sending code:
public partial class BeforePage : ContentPage
{
public BeforePage()
{
InitializeComponent();
}
private async void Button_Clicked(object sender, EventArgs e)
{
string param = myEntry.Text;
await Shell.Current.GoToAsync($"AferPage?param={param}");//like this
}
}
Here is the receiving class code(Implements IQueryAttributable interface):
public partial class AferPage : ContentPage, IQueryAttributable
{
public string param {get;set;}
public void ApplyQueryAttributes(IDictionary<string, string> query)
{
param = HttpUtility.UrlDecode(query["param"]);
receive.Text = param;
}
public AferPage()
{
InitializeComponent();
}
}
Using Newtonsoft.Json you can:
In List View:
var jsonStr = JsonConvert.SerializeObject([Model]);
await Shell.Current.GoToAsync($"{nameof([DetailsViewPage])}?Param={jsonStr }");
In Details View Page:
Add QueryProperty:
[QueryProperty(nameof(Param), nameof(Param))]
Convert to Model again:
var bonusesFilterData = JsonConvert.DeserializeObject<[Model]>(Param);
A solution is shown in this video: https://www.youtube.com/watch?v=YwnEpiJd0I8
You can pass objects through as a Dictionary.
For example if this is the data object you want to send:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the query property on the view model of the page:
[QueryProperty(nameof(Person), nameof(Person))]
public partial class DetailViewModel
{
[ObservableProperty]
Person person;
}
Assuming your page constructor looks something like this so it will automatically set the value on your context:
public partial class DetailView : ContentPage
{
public DetailView(DetailViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
Then when you navigate to the page, pass the object in a dictionary:
Routing.RegisterRoute(nameof(DetailView), typeof(DetailView));
await Shell.Current.GoToAsync(nameof(DetailView), {
new Dictionary<string, object> {
[nameof(Person)] = person
});
Now you can access your object in bindings:
<Label Text="{Binding Person.Name}"/>
<Label Text="{Binding Person.Age}"/>
P.S. Notice the use of nameof to avoid hard coded strings.
You can user stored preferences if your json is complex like:
private async void OnItemSelected(Item item)
{
if (item == null)
return;
var jsonstr = JsonConvert.SerializeObject(item);
//Clear the shared preferences in case there is any
Preferences.Clear();
//Store your complex json on a shared preference
Preferences.Set("Data", jsonstr);
await Shell.Current.GoToAsync(nameof(DetailsPage));
}
On the details page where you retrieve your data you can have the following code:
bool hasKey = Preferences.ContainsKey("Data");
var content = Preferences.Get("Data", string.Empty);
Details details = hasKey ? JsonConvert.DeserializeObject<Model>(content) : null;
I am developing an app with Xamarin.Forms, trying to leverage ReactiveUI, but Xamarin.Forms ListView does not behave as expected.
The test setup is an Entry field which I input values into. I subscribe to changes on the ReactiveList and add the value to a ListView.
The problem: The ListView updates for a few seconds and then just stops.
Console still logs inputs.
Sample code below:
<!-- xaml layouts omitted for brevity -->
<Entry x:Name="searchbox" HorizontalOptions="FillAndExpand"
Text="{Binding SearchQuery, Mode=TwoWay}"
/>
<ListView x:Name="ResultView">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Address}"></TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel + SearchResult class:
public class SearchViewModel : ReactiveObject
{
public ReactiveList<SearchResult> SearchResults { get; set; }
private string searchQuery;
public string SearchQuery
{
get { return searchQuery; }
set { this.RaiseAndSetIfChanged(ref searchQuery, value); }
}
public ReactiveCommand<List<SearchResult>> Search { get; set; }
public SearchViewModel()
{
// Set up our ListView data list
this.SearchResults = new ReactiveList<SearchResult>();
this.SearchResults.ChangeTrackingEnabled = true;
Search = ReactiveCommand.CreateAsyncTask(async _ => {
return await GenerateSearchResultAsync(this.SearchQuery);
});
Search.Subscribe(results => {
SearchResults.Clear(); // just replace output every time
SearchResults.AddRange(results);
// output results to console
results.ForEach(r => Console.WriteLine(r.Address));
});
// this used to contain a condition I removed for brevity
this.WhenAnyValue(x => x.SearchQuery).InvokeCommand(this, x => x.Search);
}
// create a new result list and return it, async code removed for demo-simplicity
private static async Task<List<SearchResult>> GenerateSearchResultAsync(string value)
{
var rv = new List<SearchResult>();
rv.Add(new SearchResult(value + " " + DateTime.Now.ToString("hh:mm:ss.FFFF")));
return rv;
}
}
public class SearchResult
{
private string address;
public SearchResult(string s)
{
this.Address = s;
}
public string Address { get; set; }
}
This seems to be a bug when WeakReferences are mistakenly collected during GC.
I'm not knowledgable enough to do the debugging, but some other smart guys did:
https://github.com/reactiveui/ReactiveUI/issues/806
https://bugzilla.xamarin.com/show_bug.cgi?id=31415 (open xamarin bug report)
Workaround:
Add this class to your project:
public class ReactiveObservableCollection<T> : ReactiveList<T>
{
public ObservableCollection<T> ObservableCollection { private set; get; }
public ReactiveObservableCollection()
{
this.ObservableCollection = new ObservableCollection<T>();
ItemsAdded.Subscribe(ObservableCollection.Add);
ItemsRemoved.Subscribe((x) => ObservableCollection.Remove(x));
}
}
and use it instead of the ReactiveList:
public class ViewModel : ReactiveObject {
public ReactiveObservableCollection<SearchResult> SearchResults { set; get; }
public ViewModel()
{
// use ReactiveObservableCollection instead of ReactiveList
this.SearchResults = new ReactiveObservableCollection<SearchResult>()
{
ChangeTrackingEnabled = true
};
}
Also, make sure to use the .ObservableCollection property on the collection as ItemSource (instead of just the collection)!
this.ResultView.ItemsSource = viewModel.SearchResults.ObservableCollection;
instead of
this.ResultView.ItemsSource = viewModel.SearchResults;
This should solve the problem.
HTH!