Xamarin.Forms binding not updating after initial value - c#

I'm binding the title of my Xamarin.Forms.ContentPage to a property BuggyTitle in my view model (VM). The VM derives from MvxViewModel. Here's the simplified version:
BuggyPage.xaml:
<?xml version="1.0" encoding="UTF-8"?>
<local:ContentPage Title="{Binding BuggyTitle}"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyProject.BuggyPage"
xmlns:local="clr-namespace:Xamarin.Forms;assembly=MyProject">
<ContentPage.Content NavigationPage.HasNavigationBar="false">
<Grid>
<ScrollView>
<!--and so on-->
</ContentPage.Content>
</local:ContentPage>
BuggyViewModel.cs:
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
public string BuggyTitle {get; set;}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
RaisePropertyChanged("BuggyTitle"); // this seems to make no difference
}
}
}
There's not much going on in the code behind other than a call to InitializeComponent() in the constructor.
The page is mapped to the VM generically in my project (not actually 'my' project, it's existing design), and it boils down to these (again, simplified) lines of code:
public static Page CreatePage(MvxViewModelRequest request)
{
var viewModelName = request.ViewModelType.Name;
var pageName = viewModelName.Replace ("ViewModel", "Page");
var pageType = (typeof (MvxPagePresentationHelpers)).GetTypeInfo ().Assembly.CreatableTypes().FirstOrDefault(t => t.Name == pageName);
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
var viewModel = viewModelLoader.LoadViewModel(request, null);
var page = Activator.CreateInstance(pageType) as Page;
page.BindingContext = viewModel;
return page;
}
The problem:
When BuggyPage loads, I initially get the correct value for the title. Whenever it is displayed after that, even though I can see in the debugger that BuggyTitle is getting updated correctly, the change does not appear in the page.
Question:
Why don't updates to BuggyTitle get reflected in the page?
Edit 1:
To further describe the weirdness, I added a Label to my ContentPage, with x:Name="BuggyLabel" and Text="{Binding BuggyLabelText}".
In my code-behind, I added this:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
BuggyLabel.Text = binding_context.BuggyLabelText;
}
I set a breakpoint at BuggyLabel.Text =. It gets hit every time the page loads, and BuggyLabel.Text already seems to have the correct value (i.e, whatever binding_context.BuggyLabelText is set to). However, the actual page displayed only ever shows what the text in this label is initially set to.
And yes, have clean/built about a million times.
Edit 2 (further weirdness):
I put this in the code-behind so that it runs during page load:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
binding_context.RefreshTitleCommand.Execute(null);
});
}
This again changes values in the debugger, but these changes don't get reflected in the displayed page.
I then added a button to the page and bound it to RefreshTitleCommand, and wham! the page updates its display.
Unfortunately I can't use this. Not only is it incredibly hackish, I can't have the user pressing buttons to have the page display what it's meant to on load.
I wonder if there's some caching going on with MvvmCross or Xamarin.

Answer
You need to add RaisePropertyChanged in BuggyTitle property declaration.
ViewModel
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() => BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
}
}
-----New Update------
Code behind code
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
BuggyLabel.Text = binding_context.BuggyLabelText;
});
}

I don't have any experience at all with Xamarin (but i do want to try it out in the future when i get as comfortable as possible with UWP), but i guess the Data Binding process should be working similar to what i am used to there ...
You are mentioning that you have no problem with the values that are set when the page first loads, however when you actually update the values there's no "linking" to the visual layer, despite at debug time you actually seeing the value being set to something completely different from it's initial state.
Since you are dealing with properties-only viewmodel (Collections for instance in UWP are another level of events which need to be exposed), RaisePropertyChanged seems like the correct choice.
What i cannot understand is if when you first create your page, the Binding which you are creating is at least specified as One-Way mode, so changes in your viewmodel properties are propagated onto your UI when their set accessor methods are called.
You are setting your page context to viewmodel (each i figure is the same as DataContext in UWP/WPF), and therefore you can actually access those properties with the {Binding } markup. But what is the default mode for this operation in Xamarin ? (in UWP it is actually OneWay, and therefore it would work right of the bat for this situation ...).
I have seen that in Xamarin it might be a bit different , since you also have the Default option. Can that be it?
PS. Hopefully this might be useful to you, despite my lack of experience with Xamarin.
Edit2
Implementing the INotifyPropertyChanged,
public class BuggyViewModel : MvxViewModel, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() =>
BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

I was setting a controls binding context from the property changed event of one of its properties.
This made my control stop tracking changes despite everything else binding correctly still, and the control would also correctly bind initially (first time is fine, further changes do not fire the property changed event again).
page.BindingContext = viewModel;

Related

How to swap data-bound page in WPF MVVM application?

I have a window that displays templates in a tree, these can be selected which updates a ListView with available fields in the template. All related operations until here are managed by the TemplateViewModel declared at windows level as:
<Window.DataContext>
<vm:TemplateViewModel/>
</Window.DataContext>
extract of the class:
public class TemplateViewModel : ViewModelBase,INotifyPropertyChanged
{
public FieldTypeViewModel FieldTypeView { get; }
public TemplateViewModel()
{
// Create additional view
FieldTypeView = new FieldTypeViewModel(this);
...
}
Each template field has an identifier and type which are still managed by this view (all working up to here).
Now depending on the type of the field a different page is to be displayed in a reserved window part (Frame). Also the type view model is a separate view model class FieldTypeView .
The FieldType object is created in the constructor of the TemplateViewModel and saved in the FieldTypeView property as it needs to be linked to this model for updating as field gets selected.
Both views used to implement the INotifyPropertyChanged interface but since the FieldTypeView is created by the view and not by the window defintion the notification event is not set, so I currently call the parent (TemplateViewModel) event for notification.
So I have a frame defined as:
<Frame DataContext="{Binding FieldTypeView}" Grid.Row="1" Content="{Binding CurrentFieldTypeSetupPage}"/>
public class FieldTypeViewModel : ViewModelBase
{
private TemplateViewModel _templateViewModel;
private TTemplateFieldType? _FieldType;
public TTemplateFieldType? FieldType
{
get { return _FieldType; }
set { _FieldType = value;
UpdateFieldType();
NotifyPropertyChanged("FieldType"); }
}
private Page? _CurrentFieldTypeSetupPage;
public Page? CurrentFieldTypeSetupPage
{
get { return _CurrentFieldTypeSetupPage; }
set { _CurrentFieldTypeSetupPage = value; NotifyPropertyChanged("CurrentFieldTypeSetupPage"); }
}
// Define property per type for easy data context access
public TTFTText? tfText { get; set; }
public TTFTDate? tfDate { get; set; }
//------------------------------------------------------------------------------
private void UpdateFieldType()
{
// Set the appropriate field type, and "null" the others
tfText = _FieldType as TTFTText;
tfDate = _FieldType as TTFTDate;
if (_FieldType != null)
{
CurrentFieldTypeSetupPage = _FieldType.GetSetupPage();
}
else
{
CurrentFieldTypeSetupPage = null;
}
}
//------------------------------------------------------------------------------
public void NotifyPropertyChanged(string prop)
{
_templateViewModel.NotifyPropertyChanged(prop);
}
//------------------------------------------------------------------------------
public FieldTypeViewModel(TemplateViewModel templateVM)
{
_templateViewModel = templateVM;
}
}
Every time the field selection changes the TemplateViewModel does set the FieldTypeView which gets the correct window for the current type and sets its CurrentFieldTypeSetupPage, which finally notifies the change via NotifyPropertyChanged("CurrentFieldTypeSetupPage"); which actually calls the TemplateViewModel's NotifyPropertyChanged method calling the event handler to notify the change.
Note that notification in the TemplateViewModel works for all its other fields, but the type page is never shown.
So the question is what I am doing wrong or what is the correct way to implement dynamic page changing in MVVM. My guess is that INotifyPropertyChange is not the correct way to go ?

RelayCommand is not refreshing execute/canexecute changes

I'm newbee in mvvm (and mvvlight of course). I have 3 modelviews (a MainWindow which have a container, and another 2 modelviews (Login and Menu)). In the LoginModelView, when the user login is successfully, this call the MenuViewModel (With Messenger.Default) changing the page in the MainWindow container. All is alright until that, then i call a Message.Default.Send sending a object from LoginModelView to MenuModelView which is correctly listened, catching the object associed and executing the method associated (ConfiguraMenu) wich define a RelayCommand (checked line by line and the method is executed without any exception) but the problem is this RelayCommand is not working until i back to the LoginViewModel and i login again. I try CommandManager.InvalidateRequerySuggested() and is not working either.
This is the code for the LoginViewModel:
//This method is called when the user press the login button. No problem with this
public void ActionVerificaUsuario()
{
Miusuario = db.getUsuario(Txtusuario, Txtpassword);
if (Miusuario.esUsuario())
{
Messenger.Default.Send(new MoveToViewMessage(Page.MenuView));
Messenger.Default.Send((UsuarioModel)Miusuario);
}
}
This code is for the MenuViewModel:
public RelayCommand AbreExeClaseCommand { get; private set; }
public MenuViewModel()
{
Messenger.Default.Register<UsuarioModel>(this, usuario_recibido => {Miusuario = usuario_recibido;ConfiguraMenu(); });
}
private void ConfiguraMenu() {
Mimenu = new MenuModel(Miusuario);
AbreExeClaseCommand = new RelayCommand(() => { Messenger.Default.Send(new MoveToViewMessage(Page.NeverReachedView)); }, () => Mimenu.Sw_reportes);
CommandManager.InvalidateRequerySuggested();
AbreExeClaseCommand.RaiseCanExecuteChanged();
}
I tried to hardcode the CanExecute with true but the Execute is still without work until back and login again.
I hope you can help me (i'm scratching my head for various days with none result).
MvvmLight provides two different RelayCommand classes in two different namespaces:
Galasoft.MvvmLight.Command
Galasoft.MvvmLight.CommandWpf
Make sure, that you are using the correct namespace Galasoft.MvvmLight.CommandWpf in your WPF application.
There was a bug in MVVMLight, which resulted in not working CanExecute() behavior. They fixed it with the new .CommandWpf namespace in MVVMLight Version V5.0.2.
You can also check out this GalaSoft blog post and the change log for further information.
You try to bind the CanExecute to a propertie.
So my guess is you didn't use RaisePropertie Changed in this propertie.
You must have something like:
public class MenuModel : ViewModelBase
{
// Other pieces of code....
private bool _sw_reportes;
public bool Sw_reportes
{
get { return _sw_reportes; }
set { _sw_reportes = value;
RaisePropertyChanged(() => Sw_reportes); }
}
}

TreeView item selection vs focus mismatch

So Microsoft has decided to make the process of programmatically selecting items in a TreeView obscenely difficult and malfunctional for some insane reason or other, and the only way to do it (since virtualization ensures that any TreeViewItem you try to select doesn't currently exist) is to create a boolean IsVisible property on whatever you want to use for your source data, which by the way has to implement INotifyPropertyChanged and include an event handler now if it didn't before, and then add an ItemContainerStyle to your TreeView binding the property to the IsVisible property of the TreeViewItem.
What this doesn't do however, is set focus to the selected item, so if for example your goal was to let the user delete tree items with the keyboard and have the focus automatically shift to the deleted item's parent so the user doesn't have to continuously tab through items or click on things to get their place back, this is nearly useless. It somehow doesn't even avoid the virtualization problem, allowing you to set the selection to something that it claims in the next line of code doesn't exist.
Here's my XAML:
and relevant C#:
public partial class MainWindow : Window
{
public ObservableCollection<TreeEdit> Source { get; set; }
public MainWindow()
{
Source = new ObservableCollection<TreeEdit>();
Source.Add(new TreeEdit("Hi"));
DataContext = this;
InitializeComponent();
}
private void KeyNav(object sender, KeyEventArgs e)
{
TreeEdit Selection = (sender as TreeView).SelectedItem as TreeEdit;
ObservableCollection<TreeEdit> TSource = (ObservableCollection<TreeEdit>)(sender as TreeView).ItemsSource;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
if (e.Key == Key.Left && Selection != null)
{
if (Selection.Parent == null)
{
TSource.Remove(Selection);
}
else
{
Selection.Parent.IsSelected = true;
((TreeViewItem)((sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent))).Focus();
Selection.Parent.Remove(Selection);
}
}
}
}
}
public class TagEdit : INotifyPropertyChanged
{
public string Name
{
get
{
return name;
}
set
{
OnPropertyChanged("Name");
name = value;
}
}
private string name;
public bool IsSelected
{
get
{
return selected;
}
set
{
OnPropertyChanged("IsSelected");
selected = value;
}
}
private bool selected;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public TagEdit Parent;
public ObservableCollection<TagEdit> Children { get; set; }
public TagEdit(string n)
{
Name = n;
Children = new ObservableCollection<TagEdit>();
}
public void Remove(TagEdit tag)
{
Children.Remove(tag);
}
}
The idea is that the user can navigate the TreeView normally with the arrow keys, then use Ctrl+Left to delete the selected item and select its parent. (sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent) often returns null. Removing it causes the proper item to be selected, but the TreeView loses focus. When it doesn't return null, I get the expected behavior.
Also, despite both the TreeView's KeyboardNavigation.TabNavigation and KeyboardNavigation.ControlTabNavigation being set to the default value of Continue, they behave differently (Tab ignores the TreeView's component elements while Ctrl+Tab steps into them).
I've been trying to get something, anything to work for almost a week now and if my opening paragraph didn't tip you off I've long since worn out my patience. Please don't tell me to try anything unless you have already, personally typed exactly what you're about to tell me to try into VisualStudio and it worked.
Apologies for the harsh tone, but this problem went beyond ridiculous and into obscene some time ago.
The WPF TreeView doesn't have a single ItemContainerGenerator. Every item in a tree view is an ItemsControl and thus has its own ItemContainerGenerator for its child items. What you really need to do is to get the grand parent of the item you are going to delete and use THAT ItemContainerGenerator to call ContainerFromItem.
It's not working because you are trying to use the top level ItemContainerGenerator which only contains the top level items.
P.S. on a friendly side note :), who deletes items with a Ctrl+Left? That's undiscoverable. Why not just do this behavior when they hit Delete?

UITableView to ObservableCollection binding breaks when the containing UIViewController is initialised for the second time

I'm using mvvmcross and xamarin to bind an ObservableCollection to a UITableView. The collection is updated in place using the Add, Remove and Move methods. These calls correctly trigger INotifyCollectionChanged events and the TableView is updated as expected the first time the view containing the table is shown. If the user navigates away from the original view as part of the normal application flow but later returns the correct data is loaded into the table but calls to add, move and remove no longer update the table.
The INotifyCollectionChanged events are still being fired when the collection is updated
If I manually subscribe to these events in my subclass of MvxStandardTableViewSource and try and call ReloadData on the UITableView still does not update
My presenter is creating a new instance of the viewmodel and view each time the page is visited.
I'm also using Xamarin-Sidebar (https://components.xamarin.com/view/sidebarnavigation) for navigation in my application with a custom presenter to load the views but as far as I can tell the view is initialised via exactly the same code path whether it's the first or subsequent visit.
My presenters Show() method looks like this:
public override void Show(MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if(NavigationFactory.CheckNavigationMode(request.PresentationValues, NavigationFactory.ClearStack))
{
MasterNavigationController.ViewControllers = new UIViewController[0];
base.Show(request);
}
else if(NavigationFactory.CheckNavigationMode(request.PresentationValues, NavigationFactory.LoadView))
{
var root = MasterNavigationController.TopViewController as RootViewController;
var view = this.CreateViewControllerFor(request) as UIViewController;
root.SidebarController.ChangeContentView(view);
}
}
else
{
base.Show(request);
}
}
The binding in my ViewController looks like this:
public override void ViewDidLoad()
{
base.ViewDidLoad();
View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
var source = new TracksTableSource(TableView, "TitleText Title; ImageUrl ImageUrl", ViewModel);
TableView.Source = source;
var set = this.CreateBindingSet<TracksViewController, TracksViewModel>();
set.Bind(source).To(vm => vm.PlaylistTable);
set.Apply();
}
And my viewmodel is as below where PlaylistTable is a subclass of ObservableCollection with the Update method using add, move and remove to keep the collection up to date.
public class TracksViewModel : MvxViewModel
{
private readonly IPlaylistService _playlistService;
private readonly IMessengerService _messengerService;
private readonly MvxSubscriptionToken _playlistToken;
public PlaylistTable PlaylistTable { get; set; }
public TracksViewModel(IPlaylistService playlistService, IMessengerService messengerService)
{
_playlistService = playlistService;
_messengerService = messengerService;
if (!messengerService.IsSubscribed<PlaylistUpdateMessage>(GetType().Name))
_playlistToken = _messengerService.Subscribe<PlaylistUpdateMessage>(OnDirtyPlaylist, GetType().Name);
}
public void Init(NavigationParameters parameters)
{
PlaylistTable = new PlaylistTable(parameters.PlaylistId);
UpdatePlaylist(parameters.PlaylistId);
}
public async void UpdatePlaylist(Guid playlistId)
{
var response = await _playlistService.Get(playlistId);
PlaylistTable.Update(new Playlist(response));
}
private void OnDirtyPlaylist(PlaylistUpdateMessage message)
{
UpdatePlaylist(message.PlaylistId);
}
}
This setup works perfectly the first time the view is initialised and updates the table correctly, it's only the second and subsequent times the view is initialised that the table fails to update. Can anyone explain why the binding fails when it appears the view is created using the same techniques in both instances?
I can post additional code if required but I believe the issue will be how I'm using the presenter since the code I've not posted from PlaylistTable functions correctly in unit tests and on first viewing.

Issues with INavigationAware in Prism 5

I'm fairly new to WPF and MVVM with Prism and I'm having an issue with Prism navigation. Each time I navigate to a particular view, I want to load a new view; however, if I enter some data or fully process some data, navigate away and come back, the existing data is always kept in the view.
I have read similar issues here on SO such as this and this. Both point to using INavigationAware or IRegionMemberLifetime. I have tried to implement both in my ViewModel; however, none seem to solve the issue for me.
If I use INavigationAware and set IsNavigationTarget to false, the view never loads. If I use IRegionMemberLifetime and set KeepAlive to false, it still retains my data.
I could post code; however, it's simple and looks like the same that's in the linked issues. Has anyone had similar issues with Prism navigation?
Any help is appreciated.
Edit - Adding sample code
In an effort to get rid of any red herrings with other code I've got, I created a new Prism app with the bare necessities. My issue with the view not displaying when navigating back has cleared up; however, the view is still retaining the entered values.
Here is some sample code:
Model:
public class SomeObject
{
public string ObjectName { get; set; }
}
I created a basic view with just two TextBox controls, 1 bound and 1 not:
<StackPanel>
<TextBlock Text="ModuleA" />
<TextBox Text="{Binding DisplayedSomeObject.ObjectName, UpdateSourceTrigger=PropertyChanged}" />
<TextBox />
</StackPanel>
ViewModel:
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
[RegionMemberLifetime(KeepAlive=false)]
public class ModuleAViewModel : BindableBase, IConfirmNavigationRequest, INavigationAware
{
private SomeObject displayedSomeObject = new SomeObject();
public SomeObject DisplayedSomeObject
{
get { return displayedSomeObject; }
set
{
displayedSomeObject = value;
}
}
[ImportingConstructor]
public ModuleAViewModel()
{
}
void IConfirmNavigationRequest.ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);
}
bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
void INavigationAware.OnNavigatedFrom(NavigationContext navigationContext)
{
}
void INavigationAware.OnNavigatedTo(NavigationContext navigationContext)
{
var newSomeObject = new SomeObject();
this.DisplayedSomeObject = newSomeObject;
}
}
When I run this, both the bound and un-bound controls retain their values when navigated back to.
Have you done some troubleshooting on Navigation? Set a breakpoint to when the page is navigated to and see how all the data is coming back in. I thought I had the same problem before and I was foolishly loading a object into my VM that was never getting destroyed.
Because of this, it appeared the VM was being kept alive, but in reality it was not. It would load like normal every time, but it would be pulling from the object that wasn't reloading.
So, set a breakpoint on the navigateTo and step through to see if the data is getting reloaded or not.
EDIT:
Looking at the code above, I believe you need to add IRegionMemberLifetime as well. I am currently using Prism 4.5, so I don't know if this has changed in 5, but I have to add that for it to actually destroy it.
public class ModuleAViewModel : BindableBase, IConfirmNavigationRequest, INavigationAware, IRegionMemberLifetime
{
...
bool IRegionMemberLifetime.KeepAlive
{
get { return false; }
}
}

Categories

Resources