Creating Dynamic TabbedPage using Prism Xamarin forms - mvvm

I am using Prism with item source, Inside each tabbed page there is a Listview for which i am unable utilize "ItemTapped" event as an event to command Behaviour to trigger a command in viewmodel when an listview item is tapped, but there is no trigger in debug mode when i tap an list view item, Please help me understand why this is happening and is there any alternate way in which i can use command for this. Also when i checked The event is getting triggered in PrismTabbedPage1.xaml.cs but not in PrismTabbedPage1ViewModel.cs
Prism Xamarin forms
PrismTabbedPage1.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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" xmlns:behaviors="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" prism:ViewModelLocator.AutowireViewModel="True" x:Class="testapp.Views.PrismTabbedPage1" ItemsSource="{Binding countries}"> <TabbedPage.ItemTemplate>
<DataTemplate>
<ContentPage Title="{Binding CountryName}">
<ListView ItemsSource="{Binding Cities}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding CityName}" TextColor="Black"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Behaviors>
<behaviors:EventToCommandBehavior EventName="ItemTapped" Command="{Binding ListItemTapped}" EventArgsParameterPath="Item"/>
</ListView.Behaviors> </ListView>
</ContentPage>
</DataTemplate>
</TabbedPage.ItemTemplate>
</TabbedPage>`
PrismTabbedPage1ViewModel.cs
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using testapp.Models;
namespace testapp.ViewModels
{
public class PrismTabbedPage1ViewModel : BindableBase
{
public ObservableCollection<Country> countries { get; set; }
public DelegateCommand<Country> ListItemTapped => new DelegateCommand<Country>(OnTapped);
private void OnTapped(Country obj)
{
//NavigationService.NavigateAsync("MainPage");
}
public PrismTabbedPage1ViewModel()
{
ObservableCollection<City> Cities1 = new ObservableCollection<City>()
{
new City(){ CityName = "City1" },new City(){ CityName = "City2" },new City(){ CityName = "City3" },
new City(){ CityName = "City4" },new City(){ CityName = "City5" },new City(){ CityName = "City6" },
new City(){ CityName = "City7" },new City(){ CityName = "City8" },new City(){ CityName = "City9" },
};
countries = new ObservableCollection<Country>()
{
new Country(){ CountryName = "Country 1", Cities = Cities1 },
new Country(){ CountryName = "Country 2", Cities = Cities1 },
new Country(){ CountryName = "Country 3", Cities = Cities1 },
};
}
}
}
Country.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
namespace testapp.Models
{
public class Country
{
public string CountryName { get; set; }
public ObservableCollection Cities { get; set; }
}
public class City
{
public string CityName { get; set; }
}
}
In this code the listview command behaviour is not get triggered.

I'm not familiar with Prism but since your EventToCommand is inside a ListView, your default binding is the data passed by the listview, and not your ViewModel.
You could try something like this :
<behaviors:EventToCommandBehavior EventName="ItemTapped"
Command="{Binding Source={x:Reference this},Path=BindingContext.ListItemTapped}"
EventArgsParameterPath="{Binding .}"/>
And give a name to your page :
<TabbedPage .... x:Name="this">
So your binding inside your listview should effectively redirect to the command in your viewmodel.
I hope this helps.

Related

Bound items display as name of item type, instead of contents of each item

//My Model
public class BookInfo
{
public string BookName { get; set; }
public string BookDescription { get; set; }
}
//my View Model
public ObservableCollection<BookInfo> Bookmodel { get; set; }
public BookRepoInfo()
{
Bookmodel = new ObservableCollection<BookInfo> { //**is this correct way.**
new BookInfo { BookName = "Object-Oriented Programming in C#", BookDescription = "Object-oriented programming is a programming paradigm based on the concept of objects" },
......
};
}
XAML page:
<ContentPage.BindingContext>
<local:BookRepoInfo />
</ContentPage.BindingContext>
<X:YList ItemsSource="{Binding Bookmodel}"></X:YList>
Load the list item using MVVM pattern
Assuming that YList is either a inheritance from ListView or CollectionView, you'll need to provide some sort of template which you want to apply to each cell of that list.
Right now what is happening is that it will just call the ToString() on the object that you put in.
Change your code to be something like:
<X:YList ItemsSource="{Binding Bookmodel}">
<X:YList.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Label Text="{Binding BookName}"/>
<Label Text="{Binding BookDescription}"/>
</VerticalStackLayout>
</DataTemplate>
</X:YList.ItemTemplate>
</X:YList>
More information is here: https://learn.microsoft.com/dotnet/maui/user-interface/controls/collectionview/populate-data?view=net-maui-7.0#define-item-appearance

How to create a footer template in a Grouped ListView in Xamarin Form

I'm working with Xamarin Form and C#. So, I have a listview in a Xamarin project which I enable the property IsGroupingEnabled. The items and header of items work perfectly but I cannot set a footer template.
I need to create a listview, items must be composed of:
Header: User information.
Items: List of subitems.
Footer: Actions buttons (shared, comments, etc).
This is part of my code:
Model:
public class Post
{
public long PostID { get; set; }
public string Name { get; set; }
public List<OptionDefault> OptionsDefault { get; set; }
}
public class OptionDefault
{
public long OptionTypeID { get; set; }
public string SubItemName { get; set; }
}
ViewModel
public class PostsViewModel
{
public InfiniteScrollCollection<Grouping<Post, OptionDefault>> Items { get; set; } = new InfiniteScrollCollection<Grouping<Post, OptionDefault>>();
public IPostsService repository;
public PostsViewModel(INavigation navigation)
{
repository = new PostsService();
Items = new InfiniteScrollCollection<Grouping<Post, OptionDefault>>
{
OnLoadMore = async () => await GetItems()
};
Items.LoadMoreAsync();
}
async Task<List<Grouping<Post, OptionDefault>>> GetItems()
{
IsWorking = true;
List<Grouping<Post, OptionDefault>> items = new List<Grouping<Post, OptionDefault>>();
try
{
// load the next page
var lists = await repository.GetList(Items.Count);
foreach (var item in lists)
{
for (int i = 0; i < item.OptionsDefault.Count; i++)
{
if ((i + 1) == item.OptionsDefault.Count)
item.OptionsDefault[i].LastItem = true;
}
var group = new Grouping<Post, OptionDefault>(item, item.OptionsDefault);
items.Add(group);
}
}
catch (Exception ex)
{
ErrorHelper.RegisterError(ex);
}
finally
{
IsWorking = false;
}
return items;
}
.xaml:
<ListView ItemsSource="{Binding Items}"
IsGroupingEnabled="True">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="5,15,5,0">
<Label Text="{Binding Key.User.UserName}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="5,0,5,5">
<Label Text="{Binding SubItemName}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I've tried using footertemplate, but it doesn't work:
<ListView.FooterTemplate>
<DataTemplate>
<StackLayout Margin="5,0,5,5">
<Label Text="This is my footer" />
</StackLayout>
</DataTemplate>
</ListView.FooterTemplate>
Whether it's <ListView.Footer> or <ListView.FooterTemplate>, the display position is the same, right at the bottom of the ListView.
Currently, ListView of xamarin form does not have this property to make each items have a footer.
Maybe you need to rethink the requirements and UI design of your app.
CollectionView is now available in Xamarin.Forms. It have both GroupHeaderTemplate and GroupFooterTemplate. You can use it to show the Group of data. The code to do so is identical with ListView
It is not <ListView.FooterTemplate> but <ListView.Footer>
<ListView.Footer>
<StackLayout Margin="5,0,5,5">
<Label Text="This is my footer" />
</StackLayout>
</ListView.Footer>
For more info, see:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance#headers-and-footers

Changing Property of ObservableCollection does nothing

In my Xamarin project I have a ListView, which gets populated by an ObservableCollection, which holds "Item" objects with some properties. If I add items to the collection the UI gets updated, but if I change only a property, it won't it does nothing. Even after an UI update through adding an item does nothing, although the property gets correctly changed. How can I make the UI refresh if a property gets changed?
BindableBase is a class from PRISM that implements INotifyPropertyChanged and DelegateCommand implements ICommand, btw.
Here's my XAML:
<ListView ItemsSource="{Binding ListItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="viewCell">
<ContentView Padding="0,0,0,5"
HeightRequest="50">
<ContentView.GestureRecognizers>
<TapGestureRecognizer BindingContext="{Binding Source={x:Reference listView}, Path=BindingContext}"
Command="{Binding ItemTappedCommand}"
CommandParameter="{Binding Source={x:Reference viewCell}, Path=BindingContext}" />
</ContentView.GestureRecognizers>
<Frame OutlineColor="{Binding Color}" Padding="8">
<StackLayout Orientation="Horizontal" >
<Image x:Name="checkedImage"
HeightRequest="30"
WidthRequest="30"
BackgroundColor="{Binding Color}"
/>
<Label Text="{Binding Text}"
TextColor="{Binding Color}"
Margin="20,0,0,0"
VerticalTextAlignment="Center"
HorizontalOptions="FillAndExpand"/>
<Image Source="{Binding Image}" />
</StackLayout>
</Frame>
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here's my ViewModel:
public class DetailPageViewModel : BindableBase
{
public DetailPageViewModel()
{
_listItems.Add(new ViewModels.Item("#123456", "Test1", "XamarinForms.Assets.Yellow.png"));
_listItems.Add(new ViewModels.Item("#654321", "Test3", "XamarinForms.Assets.close.png"));
}
private ObservableCollection<Item> _listItems = new ObservableCollection<Item>();
public ObservableCollection<Item> ListItems
{
get { return _listItems; }
set { SetProperty(ref _listItems, value); }
}
public DelegateCommand<Item> ItemTappedCommand => new DelegateCommand<Item>(ItemTapped);
private void ItemTapped(Item listItem)
{
// Adding an item refreshes the UI.
_listItems.Add(new ViewModels.Item("#654321", "Test3", "XamarinForms.Assets.close.png"));
// But changing the color of an item does nothing. Not even after an UI refresh.
_listItems.ElementAt(_listItems.IndexOf(listItem)).Color="#987654";
}
}
And my Item class:
public class Item
{
public string Color { set; get; }
public ImageSource Check { set; get; }
public string Text { private set; get; }
public ImageSource Image { private set; get; }
public Item(string color, string text, string imageSource)
{
Check = ImageSource.FromResource("XamarinForms.Assets.checkmark-outlined-verification-sign.png");
Color = color;
Text = text;
Image = ImageSource.FromResource(imageSource);
}
}
This is because also your item class needs to implement INotifyPropertyChanged. In your case as you are using Prism you just need to make your item class extend BindableBase (Prism base class which already implements INotifyPropertyChanged for you).
Link: https://github.com/PrismLibrary/Prism/blob/a60d38013c02b60807e9287db9ba7f7506af0e84/Source/Prism/Mvvm/BindableBase.cs
That should make it work.
Also I see in you are doing this:
public ObservableCollection<Item> ListItems
{
get { return _listItems; }
set { SetProperty(ref _listItems, value); }
}
With ObservableCollections you don't need to raise the event manually as they already do it internally. They can be defined as regular properties.
public ObservableCollection<Item> ListItems {get; set;}

mvvm datagrid cannot update model from view

I'm new to WPF, I am using MVVM via MVVMLight, I have a datagrid bound to a ObservableCollection of view models, When I manually update modelView, the view is changed as expected,
the problem is that when I change view, the viewModel's value is not changed,
this is model:
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
this is viewModel:
public class StudentViewModel : ViewModelBase
{
private Student _student;
public StudentViewModel(Student student)
{
_student = student;
}
public string FirstName
{
get { return _student.FirstName; }
set
{
if(_student.FirstName==value)return;
_student.FirstName = value;
RaisePropertyChanged("FirstName");
}
}
public string LastName
{
get { return _student.LastName; }
set
{
if(_student.LastName==value)return;
_student.LastName = value;
RaisePropertyChanged("LastName");
}
}
}
this is Test.xaml:
<DataGrid Name="DgrdColumns" Grid.Row="1" Grid.Column="0" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="FirstName">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FirstName, Mode=TwoWay}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="LastName" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding LastName, Mode=TwoWay}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
this is bind data code:
void BindData()
{
var students = new List<Student>()
{
new Student()
{
FirstName = "aa",
LastName = "AA"
},
new Student()
{
FirstName = "bb",
LastName = "BB"
}
};
var viewModels=new ObservableCollection<StudentViewModel>(students.Select(x=>new StudentViewModel(x)));
DgrdColumns.ItemsSource = viewModels;
}
Please try to set UpdateSourceTrigger in your bindings:
<TextBox Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
Let me know if it still doesn't work.

Xceed Datagrid loses SelectedItem when child selected

I have an Xceed datagrid in a WPF MVVM application which is set up to hold master-detail records. When a child row is selected, I want the ViewModel to detect the selected child. I would like to do this preferably with zero code-behind. I have written code which executes an action on the selected item when a contextmenu is clicked. This works correctly when a parent is selected, but always returns null when a child is selected.
I have put together a very simplified version of what I am trying to acheive:
My XAML is:
<Window x:Class="MasterDetailSelection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xcdg="clr-namespace:Xceed.Wpf.DataGrid;assembly=Xceed.Wpf.DataGrid.v4.2"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<xcdg:DataGridCollectionViewSource x:Key="cvs_parents" Source="{Binding Path=Parents}">
<xcdg:DataGridCollectionViewSource.DetailDescriptions>
<xcdg:PropertyDetailDescription RelationName="Children"
AutoCreateDetailDescriptions="False">
</xcdg:PropertyDetailDescription>
</xcdg:DataGridCollectionViewSource.DetailDescriptions>
</xcdg:DataGridCollectionViewSource>
</Grid.Resources>
<xcdg:DataGridControl x:Name="ParentGrid"
NavigationBehavior="RowOnly"
ItemsSource="{Binding Source={StaticResource cvs_parents}}"
SelectedItem="{Binding SelectedItem}"
AutoCreateDetailConfigurations="True"
ReadOnly="True">
<xcdg:DataGridControl.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Execute Command"
CommandParameter="{Binding DataContext.SelectedItem}"
Command="{Binding DataContext.SampleCommand}" />
</ContextMenu>
</xcdg:DataGridControl.ContextMenu>
</xcdg:DataGridControl>
</Grid>
</Window>
The View Model is:
namespace MasterDetailSelection
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Microsoft.Practices.Composite.Presentation.Commands;
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Parent> _parents;
public event PropertyChangedEventHandler PropertyChanged;
private DelegateCommand<Object> _sampleCommand;
private object _selectedItem;
public ObservableCollection<Parent> Parents
{
get { return _parents; }
set
{
_parents = value;
OnPropertyChanged("Parents");
}
}
public DelegateCommand<Object> SampleCommand
{
get
{
if (_sampleCommand == null)
{
_sampleCommand = new DelegateCommand<object>(ExecuteSampleCommand, CanExecuteSampleCommand);
OnPropertyChanged("SampleCommand");
}
return _sampleCommand;
}
}
public bool CanExecuteSampleCommand(Object commandParameter)
{
return true;
}
public void ExecuteSampleCommand(Object commandParameter)
{
Console.WriteLine("ExecuteSampleCommand");
}
public object SelectedItem
{
get { return _selectedItem; }
set
{
if (_selectedItem != value)
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
}
public void LoadParents()
{
var parents = new ObservableCollection<Parent>()
{
new Parent()
{
Id=1,
Description = "Parent1",
Children = new List<Child>(){new Child() {Id = 1, Description = "Child1"} }
}
};
Parents = parents;
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
There are 2 simple entities:
public class Parent
{
public int Id { get; set;}
public string Description { get; set;}
public IEnumerable<Child> Children { get; set;}
}
public class Child
{
public int Id { get; set; }
public string Description { get; set; }
}
The OnStartup override in App.xaml.cs contains the following:
var viewModel = new ViewModel();
var window = new MainWindow();
window.DataContext = viewModel;
viewModel.LoadParents();
window.Show();
Whenever i select the parent row, the SelectedItem setter is called with a populated object. When I selected a child row, the same setter is called, but with a null value.
Is there any way I can get a reference to the selected item when the context menu is clicked on a child row - and do this without code-behind. If not, is it possible with code-behind?
Perhaps you should set a context menu directly on the cells or row like below. Then you can send the appropriate model to the command. In the example below I use a static locator class that has my VM to bind the actual command too.
<Style TargetType="{x:Type xcdg:DataCell}">
<Setter Property="ContextMenu" Value="{StaticResource CellContextMenu}">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Execute Command"
CommandParameter="{Binding}"
Command="{Binding Source={StaticResource Locator} Path=ViewModel.SampleCommand}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>