GridView does not refresh changes to items when using LayoutAwarePage.DefaultViewModel - xaml

In a "Blank App" (Visual C#, Windows Store), I create a new "Grouped Items Page", then declare a MyItemViewModel class deriving from DependencyObject with a Dependency Property for a String Title.
This is the page's LoadState method:
protected async override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
this.DefaultViewModel["Groups"] = this.items;
this.items.Add(new MyItemViewModel { Title = "My Title" });
for (int i = 0; i < 5; i++)
{
await Task.Delay(1000);
this.items.First().Title += ".";
}
}
The expectation is that dots appear after the item's title every second. The actual output is simply "My Title" and nothing else happens.
By adding the following unreferenced dependency property the dots will then appear:
public MyItemViewModel blah
{
get { return (MyItemViewModel)GetValue(blahProperty); }
set { SetValue(blahProperty, value); }
}
public static readonly DependencyProperty blahProperty =
DependencyProperty.Register("blah", typeof(MyItemViewModel), typeof(GroupedItemsPage1), new PropertyMetadata(0));
Why does the GridView only refresh the Title property of the item view model when there is an unused dependency property with the same type?
Do view model classes always have to be explicitly declared as a dependency property somewhere in an app at least once?

DependencyObject is usually inherited by UIElement (e.g., Grid, TextBlock, etc.), and their properties are DependencyProperty which allows, for example, Binding.
A ViewModel should implement INotifyPropertyChanged instead of inherit from DependencyObject. If you look at sample templates like GridApp, you will see that BindableBase implements INotifyPropertyChanged.

Related

.Net Maui Shell Navigation - Is it possible to pass a Query Parameter and Auto Populate a Page?

I need to auto populate a Page by passing a Shell Navigation Parameter to a ViewModel/Method and call a Service to return a single record from a Web Service. Essentially a drill-through page. My issue is that I need to call the data retrieveal command, "GetFieldPerformanceAsync" (note [ICommand] converts this to "GetFieldPerformanceCommand") from the "To" Page's code-behind from within OnNavigatedTo. This is required since the Shell Navigation Parameter is not set in the ViewModel until the Page is loaded. I'm currently unable to make the Command call from OnNavigatedTo and need advice on how to accomplish this.
Thanks!
Code behind the Page:
public partial class FieldPerformancePage : ContentPage
{
public FieldPerformancePage(FieldPerformanceViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
//works with parameter hard-coded in ViewModel
//viewModel.GetFieldPerformanceCommand.Execute(null);
}
FieldPerformanceViewModel viewModel;
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
//this does not work
viewModel.GetFieldPerformanceCommand.Execute(null);
}
}
ViewModel
namespace TrackMate.ViewModels;
[QueryProperty(nameof(FieldAssignedWbs), nameof(FieldAssignedWbs))]
public partial class FieldPerformanceViewModel : BaseViewModel
{
[ObservableProperty]
FieldAssignedWbs fieldAssignedWbs;
[ObservableProperty]
FieldPerformance fieldPerformance;
FieldPerformanceService fieldPerformanceService;
public FieldPerformanceViewModel(FieldPerformanceService fieldStatusService)
{
Title = "Status";
this.fieldPerformanceService = fieldStatusService;
}
[ICommand]
async Task GetFieldPerformanceAsync()
{
if (IsBusy)
return;
try
{
IsBusy = true;
int wbsId = fieldAssignedWbs.WbsId;
var fieldPerformanceList = await fieldPerformanceService.GetFieldPerformanceList(wbsId);
if (fieldPerformanceList.Count != 0)
FieldPerformance = fieldPerformanceList.First();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
await Shell.Current.DisplayAlert("Error!",
$"Undable to return records: {ex.Message}", "OK");
}
finally
{
IsBusy = false;
}
}
}
I believe I figured it out...
By adding ViewModel Binding within the OnNavigatedTo method in the "DetailsPage" Code Behind, a Command Call can be made to the Page's ViewModel to execute data retrieval method after the Shell Navigation Parameter (object in this scenario) passed from the "Main" Page has been set. Note a null is passed since the Query Parameter is sourced from the ViewModel. If you are new to .Net Maui, as I am, I recommend James Montemagno's video on .Net Maui Shell Navigation.
namespace TrackMate.Views;
public partial class FieldPerformancePage : ContentPage
{
public FieldPerformancePage(FieldPerformanceViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
FieldPerformanceViewModel viewModel = (FieldPerformanceViewModel)BindingContext;
viewModel.GetFieldPerformanceCommand.Execute(null);
base.OnNavigatedTo(args);
}
}
For me it only worked when the BindingContext assignment is before the component initialization and the method call after the base call in OnNavigatedTo
public partial class OccurrencePage : ContentPage
{
public OccurrencePage(OccurrenceViewModel model)
{
BindingContext = model;
InitializeComponent();
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
OccurrenceViewModel viewModel = (OccurrenceViewModel)BindingContext;
viewModel.GetFieldsCommand.Execute(null);
}
}
While overriding OnNavigatedTo works fine, there is one more simple technique to run something once your query param is set, given you do not need to run anything asynchronous inside the method: implementing partial method OnFieldAssignedWbsChanged, auto-generated for your convenience by mvvm toolkit
partial void OnFieldAssignedWbsChanged(FieldAssignedWbs value)
{
// run synchronous post query param set actions here
}
Less amount of code and less code-behind and viewModel dependencies, but works fine for non-async operations only.

NotifyPropertyChange fired but UI field not updated in Xamarin.Forms

I'm currently refactoring a few abstraction layers into a Xamarin app in order to break the "monolithic" structure left by the previous dev, but something has gone awry. In my ViewModel, I have a few properties that call NotifyPropertyChange in order to update the UI whenever a value is picked from a list. Like so:
public Notifier : BindableObject, INotifyPropertyChanged
{
//...
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Had to create a middle layer due to my specific needs
public interface ISomeArea
{
DefinicaoServicoMobile TipoPasseio { get; set; }
}
-
public class SomeAreaImpl : Notifier, ISomeArea
{
//...
protected DefinicaoServicoMobile _tipoPasseio;
public DefinicaoServicoMobile TipoPasseio
{
get => _tipoPasseio;
set
{
if (_tipoPasseio != value)
{
_tipoPasseio = value;
NotifyPropertyChanged(nameof(TipoPasseio));
}
}
}
}
The actual bound view model:
public MyViewModel : BaseViewModel, ISomeArea
{
private SomeAreaImpl someArea;
//...
public MyViewModel()
{
// This is meant to provide interchangable areas across view models with minimal code replication
someArea = new SomeAreaImpl();
}
public DefinicaoServicoMobile TipoPasseio
{
get => someArea.TipoPasseio;
set => someArea.TipoPasseio = value;
}
}
And the .xaml snippet:
<renderers:Entry
x:Name="TxtTipoPasseio"
VerticalOptions="Center"
HeightRequest="60"
HorizontalOptions="FillAndExpand"
Text="{Binding TipoPasseio.DsPadrao}"
/>
The renderer opens a list allowing the user to choose whichever "TipoPasseio" they want, and supposedly fill the textbox with a DsPadrao (standard description). Everything works, even the reference to TipoPasseio is held after being selected (I know this because should I bring up the list a second time, it will only display the selected DsPadrao, giving the user the option to clean it. If he does, a third tap will show all the options again.
I might have screwed up in the abstraction, as I don't see the setter for myViewModel.TipoPasseio being called, tbh
Any ideas?
Let's reason through what Xamarin knows (as best as we can, since you didn't include all of the relevant code):
You have a data context having the type MyViewModel
That view model object has a property named TipoPasseio, having type DefinicaoServicoMobile
The type DefinicaoServicoMobile has a property named DsPadrao
It is that last property that is bound to the Entry.Text property.
In a binding, any observable changes to values forming the source or path for the binding will cause the runtime to update the target property for the binding (Entry.Text) and thus result in a change in the visual appearance (i.e. new text being displayed).
Note the key word observable. Here are the things I see which are observable by Xamarin:
The data context. But this doesn't change.
That's it.
With respect to the value of the MyViewModel.TipoPasseio property, there's nothing in the code you posted showing this property changing. But if it did, it doesn't look like MyViewModel implements INotifyPropertyChanged, so Xamarin wouldn't have a way to observe such a change.
On that second point, you do implement INotifyPropertyChanged in the SomeAreaImpl type. But Xamarin doesn't know anything about that object. It has no reference to it, and so has no way to subscribe to its PropertyChanged event.
Based on your statement:
I don't see the setter for myViewModel.TipoPasseio being called
That suggests that the TipoPasseio property isn't being changed. I.e. while you wouldn't be providing notification to Xamarin even if it did change, it's not changing anyway.
One property that does seem to be changing is the DsPadrao property (after all, it's the property that's actually providing the value for the binding). And while you don't provide enough details for us to know for sure, it seems like a reasonable guess that the DefinicaoServicoMobile doesn't implement INotifyPropertyChanged, and so there's no way for Xamarin to ever find out the value of that property might have changed either.
In other words, of all the things that Xamarin can see, the only one that it would be notified about of a change is the data context. And that doesn't seem to be what's changing in your scenario. None of the other values are held by properties backed by INotifyPropertyChanged.
Without a complete code example, it's impossible to know for sure what the right fix is. Depending on what's changing and how though, you need to implement INotifyPropertyChanged for one or more of your types that don't currently do so.
As it turns out, I wasn't firing the NotifyPropertyChanged of the correct object. Both MyViewModel and SomeAreaImpl implemented INotifyPropertyChanged per the Notifier class as BaseViewModel also extends from Notifier but that ended up ommited in my question. Having figured that out, here's an working (and complete) example:
public Notifier : BindableObject, INotifyPropertyChanged
{
//...
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Specifics about DefinicaoServicoMobile are negligible to this issue
public interface ISomeArea
{
//...
DefinicaoServicoMobile TipoPasseio { get; set; }
Task SetServico(ServicoMobile servicoAtual;
//...
}
For the sake of clarification
public abstract class BaseViewModel : Notifier
{
protected abstract Task SetServico(ServicoMobile servicoAtual);
public async Task SetServico()
{
//...
await SetServico(servicoAtual);
//...
}
}
Changed a couple of things here. It no longer extends from Notifier, which was kinda weird to begin with. Also this is where I assign TipoPasseio
public class SomeAreaImpl : ISomeArea
{
//...
protected DefinicaoServicoMobile _tipoPasseio;
// I need to call the viewModel's Notifier, as this is the bound object
private BaseViewModel viewModel;
public AreaServicosDependentesImpl(BaseViewModel viewModel)
{
this.viewModel = viewModel;
}
public DefinicaoServicoMobile TipoPasseio
{
get => _tipoPasseio;
set
{
if (_tipoPasseio != value)
{
_tipoPasseio = value;
viewModel.NotifyPropertyChanged(nameof(TipoPasseio));
}
}
}
//Assigning to the property
public async Task SetServico(ServicoMobile servicoAtual, List<DefinicaoServicoMobile> listDefinicaoServico)
{
//...
TipoPasseio = listDefinicaoServico
.FirstOrDefault(x => x.CdServico == servicoAtual.TpPasseio.Value);
//...
}
}
Changes to the view model:
public MyViewModel : BaseViewModel, ISomeArea
{
private SomeAreaImpl someArea;
//...
public MyViewModel()
{
someArea = new SomeAreaImpl(this);
}
public DefinicaoServicoMobile TipoPasseio
{
get => someArea.TipoPasseio;
set => someArea.TipoPasseio = value;
}
protected override async Task SetServico(ServicoMobile servicoAtual)
{
//...
someArea.SetServico(servicoAtual, ListDefinicaoServico.ToList());
//...
}
}
View model binding
public abstract class BaseEncerrarPontoRotaPage : BasePage
{
private Type viewModelRuntimeType;
public BaseEncerrarPontoRotaPage(Type viewModelRuntimeType)
{
this.viewModelRuntimeType = viewModelRuntimeType;
}
private async Task BindContext(PontoRotaMobile pontoRota, ServicoMovelMobile servicoMovel, bool finalizar)
{
_viewModel = (BaseViewModel)Activator.CreateInstance(viewModelRuntimeType, new object[] { pontoRota, UserDialogs.Instance });
//...
await _viewModel.SetServico();
//...
BindingContext = _viewModel;
}
public static BaseEncerrarPontoRotaPage Create(EnumAcaoServicoType enumType)
{
Type pageType = enumType.GetCustomAttribute<EnumAcaoServicoType, PageRuntimeTypeAttribute>();
Type viewModelType = enumType.GetCustomAttribute<EnumAcaoServicoType, ViewModelRuntimeTypeAttribute>();
return (BaseEncerrarPontoRotaPage)Activator.CreateInstance(pageType, new object[] { viewModelType });
}
}
Page instantiation is performed in some other view model, not related to the structure presented here
private async Task ShowEdit(bool finalizar)
{
await Task.Run(async () =>
{
var idAcaoServico = ServicoMobileAtual.DefinicaoServicoMobile.IdAcaoServico;
var page = BaseEncerrarPontoRotaPage.Create((EnumAcaoServicoType)idAcaoServico);
await page.BindContext(PontoRotaAtual, ServicoMovelMobileAtual, finalizar);
BeginInvokeOnMainThread(async () =>
{
await App.Navigation.PushAsync(page);
});
});
}
Codebehind:
public partial class MyPage : BaseEncerrarPontoRotaPage
{
public NormalUnidadePage() { }
public MyPage(Type viewModelType) : base(viewModelType)
{
InitializeComponent();
//Subscription to show the list
TxtTipoPasseio.Focused += TxtTipoPasseio_OnFocused;
//...
}
}
XAML
<views:BaseEncerrarPontoRotaPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:My.Name.Space.;assembly=Phoenix.AS"
x:Class="My.Name.Space.MyPage">
//...
<renderers:Entry
x:Name="TxtTipoPasseio"
VerticalOptions="Center"
HeightRequest="60"
HorizontalOptions="FillAndExpand"
Text="{Binding TipoPasseio.DsPadrao}"/>
//...
</views:BaseEncerrarPontoRotaPage>
I know could propagate an event from the AreaImpl classes in order to fire the Notify event in the view model, but right now I'm satisfied with this solution.

Bind to an Item of a Dependency Collection

I'm trying to create a custom control that has a header and a footer and body. The idea is that the body of the report is a custom stack panel control that will allow the user to indicate page orientation and grouping. I created a dependency property on the custom UC to accept an IList of the custom stack panel. What I am trying to do is bind to one of the stack panels in the list. But for some reason the binding is not working.
The ReportPage:
public class ReportPage : StackPanel
{
//Nothing right now but will eventually include controls for page orientation and size (8.5x11, 11x17, etc.)
}
The UserControl code behind:
public partial class Report : UserControl, INotifyPropertyChanged
{
public Report()
{
ReportPages = new List<ReportPage>();
}
public static readonly DependencyProperty =
DependencyProperty.Register("ReportPages", typeof(IList), typeof(Report));
public IList ReportPages
{
get => (IList)GetValue(ReportPagesProperty);
set
{
SetValue(ReportPagesProperty, value);
ActivePage = value[0];
OnPropertyChanged(nameof(ActivePage));
}
}
private ReportPage _activePage;
public ReportPage ActivePage
{
get => _activePage;
set
{
_activePage = value;
OnPropertyChanged(nameof(ActivePage));
}
{
}
The UserControl xaml:
<Grid>
<!--Some xaml for the header and footer.-->
<ContentControl Content="{Binding ActivePage, RelativeSource={RelativeSource, FindAncestor, AncestorType=local:Report}}"/>
</Grid>
Here is how I am consuming the custom control. This should, in my mind at least, make three "pages" which I can toggle between using a button control that I didn't share.
<reportEngine:Report>
<reportEngine:Report.ReportPages>
<reportEngine:ReportPage>
<TextBlock>This is Page 1</TextBlock>
</reportEngine:ReportPage>
<reportEngine:ReportPage>
<TextBlock>This is Page 2</TextBlock>
</reportEngine:ReportPage>
<reportEngine:ReportPage>
<TextBlock>This is Page 3</TextBlock>
</reportEngine:ReportPage>
</reportEngine:Report.ReportPages>
</reportEngine:Report>
Any Ideas why the binding isn't working?
So I at least found a quick work around. I utilized the Collection Changed Event handler pattern from this answer and modified it for static dependency properties. Then, to get the values from the collection bound to the dependency property I create a static instance of the Report object in the constructor and use that to pass various values back to the object from the collection. Something like this:
public partial class Report : UserControl, INotifyPropertyChanged
{
private static Report _thisReport;
public Report()
{
InitializeComponent();
ReportPages = new ObservableCollection<ReportPage>();
_thisReport = this;
}
public static readonly DependencyProperty ReportPagesProperty =
DependencyProperty.Register("ReportPages", typeof(IList), typeof(Report), new FrameworkPropertyMetadata(ReportPagesChanged));
public IList ReportPages
{
get => (IList)GetValue(ReportPagesProperty);
set
{
SetValue(ReportPagesProperty, value);
//Update some other properties associated with the control (Total Page Numbers, etc.)
}
}
private static void ReportPagesChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
var newColl = (INotifyCollectionChanged)eventArgs.NewValue;
if (newColl != null)
newColl.CollectionChanged += ReportPages_CollectionChanged;
var oldColl = (INotifyCollectionChanged)eventArgs.OldValue;
if (oldColl != null)
oldColl.CollectionChanged -= ReportPages_CollectionChanged;
}
private static void ReportPages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs eventArgs)
{
var newPages = (IList<ReportPage>) sender;
//Updates properties of the Report control.
_thisReport.ActivePage = newPages[0];
_thisReport.TotalPageNumber = newPages.Count;
}
}
Whether this is "correct" or not I couldn't say, but it works. If someone has a better answer I will change the answer.

Updating a GridView after adding an item to a nested list

While I was developing a startscreen for my app using the GridView control, I run into a problem. I have a GridView on my main screen which has a CollectionViewSource set as ItemSource.
For this CollectionViewSource the source is set to an ObservableCollection list. Each GroupViewModel has a ObservableCollection in it. In code the important parts looks like the following:
public class StartPageViewModel : ViewModelBase
{
public ObservableCollection<GroupViewModel> Groups { get; set; }
public CollectionViewSource GroupsCvs { get; set; }
public StartPageViewModel()
{
// fill Groups with some mock data
GroupsCvs.Source = Groups;
GroupsCvs.IsSourceGrouped = true;
}
public void MoveItems(GroupViewModel grp)
{
// add a dummy item
grp.AddRecipe(new ItemViewModel(new Item()) { Id = "123" });
RaisePropertyChanged("GroupsCvs");
RaisePropertyChanged("Groups");
}
}
public class GroupViewModel : ViewModelBase, IEnumerable<ItemViewModel>
{
public ObservableCollection<ItemViewModel> Items { get; set; }
}
View:
public sealed partial class MainPage : LayoutAwarePage
{
private ViewModelLocator locator = new ViewModelLocator();
public MainPage()
{
this.InitializeComponent();
this.DataContext = locator.Main; // returns StartPageViewModel
}
}
XAML part for MainPage, GridView
<GridView ItemsSource="{Binding GroupsCvs.View}" ...
</GridView>
How is it possible to get the UI refreshed when I add an Item to a Group's collection? In my StartPageViewModel I'm adding dummy item to the GroupViewModel and I raise propertychanged, but the Grid remains the same.
I've also tried to fire property changed event in the GroupViewModel class, when the Items collection changes without any luck.
Edit: As I wrote in comments it's possible to refresh with reassigning the source property however this gets the GridView rendered again which is not nice. I'm looking to options which would result in a nicer user experience.
I suppose CollectionViewSource doesn't react to PropertyChanged event. Try reassigning Source to GroupCvs after you modify it. It's not elegant but it should work:
GroupsCvs.Source = Groups;
As a last resort you could create a new instance of ObservableCollection<GroupViewModel> before reassigning it:
Groups = new ObservableCollection<GroupViewModel>(Groups)
GroupsCvs.Source = Groups;
<GridView ItemsSource="{Binding GroupsCvs.View, **BindingMode=TwoWay**}" ...
</GridView>

Why does FlipView ignore SelectedItem

I'd like to use a FlipView to display some items and start showing a specific item.
For this, I have defined a view model class:
class MyDataContext
{
public MyDataContext()
{
Items = new List<MyClass>();
Items.Add(new MyClass("1"));
Items.Add(new MyClass("2"));
Items.Add(new MyClass("3"));
SelectedItem = Items[1];
}
public List<MyClass> Items { get; set; }
public MyClass SelectedItem { get; set; }
}
As you can see, the selected item is not the first item.
Now for the XAML:
<FlipView ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}"></FlipView>
However, when I run the app, the flip view shows the first item, not the second item.
Is this intentional?, or is it a bug?
Try this
<FlipView
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
your SelectedItem needs to be a TwoWay binding for it to work, since the value is set by both the control and the view model.
Was having the same issue with the FlipView and unable to get the BindableBase or the TwoWay option to work. Because the order of the list was not really a topic for me, I've created a method to reorder the ItemsSource, to start with the SelectedItem as being the first item in the Collection.
In the underlying code, the result is the new ItemsSource for the FlipView, instead of the previous List elements.
public static List<T> ReorderList(List<T> elements, T selectedElement)
{
var elementIndex = elements.FindIndex(x => x.Id == selectedElement.Id);
var result = new List<T>();
foreach (var item in elements)
{
if (elementIndex .Equals(elements.Count))
{
elementIndex = 0;
}
result.Add(elements[elementIndex]);
elementIndex++;
}
return result;
}
On top of what Filip stated, you're class (MyDataContext) needs to notify the UI that the property has changed. Your ViewModel must implement INotifyPropertyChanged and the property needs to fire the PropertyChanged event
public class ViewModel : INotifyPropertyChanged
{
private object _selectedItem;
public object SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
}
You can also use the BindableBase class that comes with the sample apps
public class ViewModel : BindableBase
{
private object _selectedItem;
public object SelectedItem
{
get { return this._selectedItem; }
set { this.SetProperty(ref this._selectedItem, value); }
}
}
It's look like a bug.
If you debug your code you will notice that at first your SelectedItem in VM set to the right element, then it sets to null and after that it sets to the first element of FlipView's ItemsSource collection.
As a workaround I see setting SelectedItem of VM after Loaded event of FlipView is raised.