GridView doesn't show items async loader, doesn´t refresh items - xaml

I'm developing a Win8 App:
I have a datasource class that take ítems from webservice via json:
public class DataSourceCapitulos
{
public ObservableCollection<capitulo> ListaCapitulos { get; set; }
public DataSourceCapitulos()
{
CargarCapitulos();
}
public async void CargarCapitulos()
{
var resourceUri = Cie10Uri.CapitulosUri;
HttpClient httpClient = new HttpClient();
bool error = false;
HttpRequestException exception = null;
try
{
string response = await httpClient.GetStringAsync(resourceUri);
ListaCapitulos = new ObservableCollection<capitulo>(JsonConvert.DeserializeObject<List<capitulo>>(response));
}
catch (HttpRequestException e)
{
error = true;
exception = e;
}
if (error)
{
MessageDialog adv = new MessageDialog(string.Format("La consulta {0}, devolvió:{1}", resourceUri, exception.Message), "No se pudo consultar!!! ");
adv.Commands.Add(
new UICommand("Ok")
);
await adv.ShowAsync();
}
}
}
And have a XAML form that has this source:
<Page.Resources>
<data:DataSourceCapitulos x:Key="DataSourceCapitulos"></data:DataSourceCapitulos>
</Page.Resources>
And Finally a GridView with it's source ítems pointing to ListaCapitulos property of DataSourceCapitulos as this:
<GridView Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Source={StaticResource DataSourceCapitulos},Path=ListaCapitulos}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Height="250" Width="250">
<Grid.Background>
<ImageBrush ImageSource="{Binding Imagen}"/>
</Grid.Background>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="90"/>
</Grid.RowDefinitions>
<Rectangle >
<Rectangle.Fill>
<SolidColorBrush Color="#FF122951" Opacity="0.6"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
Until Here my app run without problema, but the problem is it doesn´t show te ítems, even the ListaCapitulos was populated as I expected.
THis is MainPage.xaaml.cs
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
/// <summary>
/// Se invoca cuando esta página se va a mostrar en un objeto Frame.
/// </summary>
/// <param name="e">Datos de evento que describen cómo se llegó a esta página. La propiedad Parameter
/// se usa normalmente para configurar la página.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
}
}
There is something that i Miss ?

In XAML, set ItemsSource="{Binding listaCapitulos} (the ObservableCollection).
in the C# file of the page you need an instance of class DataSourceCapitulos, like DataSourceCapitulos ChaptersVm = new DataSourceCapitulos(); (as a member, you need to access it more than once).
set the gridview's (the one you have in XAML which should show the chapters) DataContext to that instance. You can do this in C#, ChaptersGridView.DataContext = ChaptersVm.
You're saying you know you have the chapters in ListaCapitulos, that means you didn't bind them correctly to be shown.
Also, I see async void CargarCapitulos() is meant to be async and you are calling it in the constructor but will run synchronously. Your app might not run fluidly when gathering the chapters.
UPDATE
Altough I am not very sure of first way, I can think of two methods you can keep the call of Cargar Capitulos async.
1) Await the method call in a new method (I am not very sure this would do the trick) :
In constructor :
public DataSourceCapitulos()
{
LoadCapitulos();
}
where in LoadCapitulos body you have :
public async void LoadCapitulos()
{
//this awaits the chapters to load (you were missing await,
//but you can't use await in a constructor, so this is a work-around
await CargarCapitulos();
}
If 1) doesn't work try 2) which will work definitely but you must do some changes :
2) Your ObservableCollection will not notify the UI of any update if you do some changes on the collection with the chapters. To do so, you must use the INotifyPropertyChanged interface and implement its members. If you used an advanced template of the Windows 8 App, in the Common folder, you have a class called BindableBase - try to use it directly by inheriting it. Also, you must use the OnPropertyChanged() event call in the ObservableCollectionsetter, so that when the chapters collection changed, the UI will also be changed through binding :
public class DataSourceCapitulos : BindableBase
{
private ObservableCollection <capitulo> _listaCapitulos;
public ObservableCollection <capitulo> ListaCapitulos
{
get
{
return _listaCapitulos;
}
set
{
_listaCapitulos = value;
OnPropertyChanged(); //This notifies of changes of collection
}
}
However, if you don't have the BindableBase, just implement INotifyPropertyChanged and the rest will look just like I wrote before. With these changes that provide notifications, the chapters should show on the UI, even if they are loaded later, after the UI has loaded.
But I'd say you SHOULD implement it for the notification changes to have any effect. Otherwise, you could have used a simple List of Capitulos (freaky spanish) instead of Observable Collection.

I think the view(xaml form) is not getting aware of data change through this binding.
Why you are using a static resource?
you can straightly set ListaCapitulos to the girid's data source ,otherwise, you should use a MVVM model ,and you should aware your view of property change.

Related

How do you pass parameters in MAUI without using a ViewModel?

I have this on one page:
await Shell.Current.GoToAsync(nameof(Pages.StartPage), true, new Dictionary<string, object>
{
{ "LoginData", result }
});
result is an object/class
In my Pages.StartPage I want to get that object. I have tried using [QueryProperty... but that always returns a null. E.g.
[QueryProperty(nameof(GetLoginData), "LoginData")]
public partial class StartPage : ContentPage
...
private JsonApiResult GetLoginData { set { _loginData = value; } }
I've just started using MAUI, and I am converting an app from Xamarin to MAUI. The pages I have built take care of themselves, so I don't want to use ViewModels, I just need a value from that passed-in object for the page to do its stuff. I don't want to have to rewrite all my pages unless there is no other way
Any help would be much appreciated. I've watched loads of videos on this, and I can't make it work, what am I missing?
UPDATE
I should add that to make matters more complex for myself, I am also using Dependency Injection (DI)
here it comes an example!
Main page .xaml:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
On main page .cs:
public MainPage()
{
InitializeComponent();
}
private async void OnCounterClicked(object sender, EventArgs e)
{
List<Student> myStudentsList = new List<Student>
{
new Student {Name="Carlos",Course="IT",Age=18},
new Student {Name="Juan",Course="IT",Age=19},
new Student {Name="Leandro",Course="IT",Age=20}
};
await Navigation.PushAsync(new PageWithStudentsList(myStudentsList));
}
PageWithStudentsList .cs :
public partial class PageWithStudentsList : ContentPage
{
public PageWithStudentsList(List<Student> students)
{
Console.WriteLine(students);
InitializeComponent();
}
}
And you dont need to use viewmodel!
EDIT: in case you need another example with SHELL NAVIGATION, here is a microsoft example in their docs! Hope it helps!
private JsonApiResult _loginData;
public JsonApiResult LoginGetData {
get => _loginData;
set { _loginData = value; }
}
It seems this was the solution though I can't see why. I'll dig into it another time but right now its working so I can crack on

.Net Maui MVVM - What is the best approach to populating a CollectionView upon a Page/View opening?

I'm new to .Net Maui but have completed James Montemagno's 4 hour Workshop. Included in the Workshop was:
Creating a Page with a CollectionView
Creating a ViewModel
Creating an async method which calls a data service to retrieve data
Configuring the async method as a ICommand
Binding the data model list to the CollectionView
Binding the Command to a Button
Clicking the button works and populates the CollectionView. How would I go about removing the button and performing this action when the page opens? Note I tried modifying the method by removing the "[ICommand]" which did not work. Also, should this action be done in the Code Behind or in the ViewModel?
Thanks in advance for assistance!
(ModelView)
public partial class FieldAssignedWbsViewModel : BaseViewModel
{
FieldAssignedWbsService fieldAssignedWbsService;
public ObservableCollection<FieldAssignedWbs> WbsList { get; set; } = new();
public FieldAssignedWbsViewModel(FieldAssignedWbsService fieldAssignedWbsService)
{
Title = "Wbs Assigned";
this.fieldAssignedWbsService = fieldAssignedWbsService;
}
[ICommand]
async Task GetFieldAssignedWbsListAsync()
{
if (IsBusy)
return;
try
{
IsBusy = true;
var wbsList = await fieldAssignedWbsService.GetFieldAssignedWbsList();
if (WbsList.Count != 0)
WbsList.Clear();
foreach (var wbs in wbsList)
WbsList.Add(wbs);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
await Shell.Current.DisplayAlert("Error!",
$"Undable to get monkeys: {ex.Message}", "OK");
}
finally
{
IsBusy = false;
}
}
}
(CollectionView Binding)
<CollectionView BackgroundColor="Transparent"
ItemsSource="{Binding WbsList}"
SelectionMode="None">
(Code behind page with incorrect call to Command Method)
public partial class FieldAssignedWbsPage : ContentPage
{
public FieldAssignedWbsPage(FieldAssignedWbsViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
//The following call does not work
//Hover message: Non-invocable member... cannot be called like a method
await viewModel.GetFieldAssignedWbsListCommand();
}
}
Although the original answer is very valid, I'd recommend installing the CommunityToolkit.Maui (by Microsoft), then using its EventToCommand features.
After installing, add builder.UseMauiCommunityToolkit() to CreateMauiApp() method in MauiProgram.cs.
Then, in relevant XAML page, add this namespace xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" and then you should be able to use this block of code to do what you want:
<ContentPage.Behaviors>
<toolkit:EventToCommandBehavior
EventName="Appearing"
Command="{Binding GetFieldAssignedWbsListCommand}" />
</ContentPage.Behaviors>
Sorry that this is a bit late, I just believe that it is a slightly cleaner solution as it avoids populating the code-behind with any code and keeps UI handling purely between the viewmodel and the view.
use OnAppearing. You may also need to make the GetFieldAssignedWbsList public
protected override async void OnAppearing()
{
await viewModel.GetFieldAssignedWbsListCommand.Execute(null);
}

Use RelayCommand with not only buttons

I am using MVVM Light in my project and I am wondering if there is any way to use RelayCommand with all controls (ListView or Grid, for example).
Here is my current code:
private void Item_Tapped(object sender, TappedRoutedEventArgs e)
{
var currentItem = (TechItem)GridControl.SelectedItem;
if(currentItem != null)
Frame.Navigate(typeof(TechItem), currentItem);
}
I want to move this code to Model and use RelayCommand, but the ListView, Grid and other controls don't have Command and CommandParameter attributes.
What does MVVM Light offer to do in such cases?
Following on from the link har07 posted this might be of some use to you as I see you mention CommandParameter.
It is possible to send the "Tapped" item in the list to the relay command as a parameter using a custom converter.
<ListView
x:Name="MyListView"
ItemsSource="{Binding MyCollection}"
ItemTemplate="{StaticResource MyTemplate}"
IsItemClickEnabled="True">
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction Command="{Binding ViewInMoreDetail}" InputConverter="{StaticResource TapConverter}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
</ListView>
Custom converter class
public class TapConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var args = value as ItemClickEventArgs;
if (args != null)
return args.ClickedItem;
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
In your view model you then have a relaycommand.
public RelayCommand<MyObject> MyRelayCommand
{
get;
private set;
}
In your constructor initialise the relay command and the method you want to fire when a tap happens.
MyRelayCommand = new RelayCommand<MyObject>(HandleTap);
This method receives the object that has been tapped in the listview as a parameter.
private void HandleTap(MyObject obj)
{
// obj is the object that was tapped in the listview.
}
Don't forget to add the TapConverter to your App.xaml
<MyConverters:TapConverter x:Key="TapConverter" />

Windows 8.1: Behaviors on Flyouts don't Work

I am developing a windows 8.1 app using VS 2013 and MVVM Light.
The following code shows the behavior in a flyout within an appbar:
<AppBarButton.Flyout>
<Flyout x:Name="FlyoutCalculator"
Placement="Top"
FlyoutPresenterStyle="{StaticResource FlyoutPresenterBaseStyle}">
<uc:Calculator ApplyCommand="{Binding CancelCommand}"
CancelCommand="{Binding CancelCommand}"
Available="{Binding AvailableCounter, Mode=OneWay}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Opening">
<core:InvokeCommandAction Command="{Binding ShowCurrentCostsCommand}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Flyout>
</AppBarButton.Flyout>
Unfortunately I get an exception while compiling the app:
WinRT-Informationen: Cannot add instance of type Microsoft.Xaml.Interactions.Core.EventTriggerBehavior to a collection of type Microsoft.Xaml.Interactivity.BehaviorCollection
Other Behaviors in the View do work, does someone know a solution to this?
Extremely late answer here, but I had the same issue and came up with a solution after finding this post.
I just created a custom behavior specifically for flyouts, used like this. OpenActions will execute when the flyout is opened, and CloseActions will execute when the flyout closes. In this case, I wanted the bottom app bar to not be visible when the flyout was open.
<Flyout Placement="Full">
<i:Interaction.Behaviors>
<behaviors:FlyoutBehavior>
<behaviors:FlyoutBehavior.OpenActions>
<core:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" TargetObject="{Binding ElementName=CommandBar}" />
</behaviors:FlyoutBehavior.OpenActions>
<behaviors:FlyoutBehavior.CloseActions>
<core:ChangePropertyAction PropertyName="Visibility" Value="Visible" TargetObject="{Binding ElementName=CommandBar}" />
</behaviors:FlyoutBehavior.CloseActions>
</behaviors:FlyoutBehavior>
</i:Interaction.Behaviors>
<Grid>
...
</Grid>
</Flyout>
Code is here:
class FlyoutBehavior : DependencyObject, IBehavior
{
public DependencyObject AssociatedObject { get; private set; }
public void Attach(Windows.UI.Xaml.DependencyObject associatedObject)
{
var flyout = associatedObject as FlyoutBase;
if (flyout == null)
throw new ArgumentException("FlyoutBehavior can be attached only to FlyoutBase");
AssociatedObject = associatedObject;
flyout.Opened += FlyoutOpened;
flyout.Closed += FlyoutClosed;
}
public void Detach()
{
var flyout = AssociatedObject as FlyoutBase;
if (flyout != null)
{
flyout.Opened -= FlyoutOpened;
flyout.Closed -= FlyoutClosed;
}
}
public static readonly DependencyProperty OpenActionsProperty =
DependencyProperty.Register("OpenActions", typeof(ActionCollection), typeof(FlyoutBehavior), new PropertyMetadata(null));
public ActionCollection OpenActions
{
get { return GetValue(OpenActionsProperty) as ActionCollection; }
set { SetValue(OpenActionsProperty, value); }
}
public static readonly DependencyProperty CloseActionsProperty =
DependencyProperty.Register("CloseActions", typeof(ActionCollection), typeof(FlyoutBehavior), new PropertyMetadata(null));
public ActionCollection CloseActions
{
get { return GetValue(CloseActionsProperty) as ActionCollection; }
set { SetValue(CloseActionsProperty, value); }
}
private void FlyoutOpened(object sender, object e)
{
foreach (IAction action in OpenActions)
{
action.Execute(AssociatedObject, null);
}
}
private void FlyoutClosed(object sender, object e)
{
foreach (IAction action in CloseActions)
{
action.Execute(AssociatedObject, null);
}
}
public FlyoutBehavior()
{
OpenActions = new ActionCollection();
CloseActions = new ActionCollection();
}
}
I do not have a solution but:
I'm not using Flyouts in my Windows 8.1 App, I'm using a UserControl on which I have added a EventTriggerBehavior as you did. And I get exactly the same Errormessage from VisualStudio at runtime.
As I am using a RoutedEventHandler this could cause the Problem as you use
EventHandler<object> Opening
as the Trigger for the Behavior. But that is just an idea of what is the problem.
For me I have found an answer:
I have changed the Type of my RoutedEventHandler to be just a normal EventHandler. And the Method inside the CodeBehind which triggers the RoutedEventHandler is invoked with only the sender, because I dont know how to convert RoutedEventArgs into EventArgs, but as long as I dont need the EventArgs it's not a problem.
You could also make a workaround by creating a UserControl with a Flyout Control and make the Opening Event public to the Page where you use it. Then you can add the EventTriggerBehavior to the UserControl and connect it to your custom Opening Event and you should get the expected behavior.

WinRT XAML Toolkit BindableSelections not updating UI

Below is the xaml and c# code for handling selected items in my gridview.
I am also using MVVM Light and everything is working, including me being able to see what's inside SelectedItems.
However I when I attempt to clear the SelectedItems, my UI doesn't seem to update/reflect the changes made to SelectedItems.
I am using WinRT XAML Toolkit (http://winrtxamltoolkit.codeplex.com/) which has the BindableSelection extension on a GridView
XAML
<controls:CustomGridView
x:Name="VideoItemGridView"
Grid.Row="2"
Margin="0,-3,0,0"
Padding="116,0,40,46"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsItemClickEnabled="True"
SelectionMode="Extended"
Extensions:GridViewExtensions.BindableSelection="{Binding SelectedVideoItems, Mode=TwoWay}"
ItemsSource="{Binding Source={StaticResource ViewSource}}"
ItemTemplate="{StaticResource VideoItemTemplate}">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemWidth="250" ItemHeight="160" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</controls:CustomGridView>
MyViewViewModel.cs
#region Selected Items
/// <summary>
/// Gets or sets the selected video items.
/// </summary>
public ObservableCollection<object> SelectedVideoItems
{
get { return this._selectedVideoItems; }
set
{
this._selectedVideoItems = value;
this.Set("SelectedVideoItems", ref this._selectedVideoItems, value);
}
}
private ObservableCollection<object> _selectedVideoItems = new ObservableCollection<object>();
#endregion
#region App Bar Click Commands
/// <summary>
/// Gets the ClearSelection click command.
/// </summary>
public ICommand ClearSelectionClickCommand
{
get
{
return new RelayCommand(() => this.ClearSelectionOperation());
}
}
/// <summary>
/// Selects all command operation.
/// </summary>
private void ClearSelectionOperation()
{
this.SelectedVideoItems = new ObservableCollection<object>();
}
#endregion
Try clearing your selected items in ClearSelectionOperation by calling
this.SelectedVideoItems.Clear();
instead of
this.SelectedVideoItems = new ObservableCollection<object>();
If that doesn't help check if the current version of the extension from March 7 fixes the problem.
It turns out that since I am using a data template, it is actually my data model that needed to set a flag to indicate it is selected
Here's the missing piece of the puzzle. Once I update the data model bound to the grid view item (which also includes support for row/col spanning), the UI updated as expected.
Hope this helps others.
public class CustomGridView : GridView
{
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
try
{
base.PrepareContainerForItemOverride(element, item);
dynamic _Item = item;
element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, _Item.ColumnSpan);
element.SetValue(VariableSizedWrapGrid.RowSpanProperty, _Item.RowSpan);
element.SetValue(GridViewItem.IsSelectedProperty, _Item.IsSelected);
}
catch
{
element.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, 1);
element.SetValue(VariableSizedWrapGrid.RowSpanProperty, 1);
element.SetValue(GridViewItem.IsSelectedProperty, false);
}
finally
{
base.PrepareContainerForItemOverride(element, item);
}
}