I simply changed my ListView to use grouping, but now I can't use ScrollTo anymore.
I have create a simple app, so you can see the problem.
The XAML-page looks like (I am not using XAML in my app at the moment, but I will in an upcoming version).
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ScrollListExample"
x:Class="ScrollListExample.ProjectPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView x:Name="ProjectsListView" HasUnevenRows="True" IsGroupingEnabled="True" ItemsSource="{Binding Projects}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Path=Key}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" LineBreakMode="TailTruncation" Text="{Binding Path=ProjectName}" />
<Label Grid.Column="0" Grid.Row="1" Text="{Binding Path=ProjectReference, StringFormat='Sag: {0}'}" />
<Label Grid.Column="0" Grid.Row="2" Text="{Binding Path=CustomerName}" />
<Label Grid.Column="0" Grid.Row="3" Text="{Binding Path=FullAddress}" />
<Label Grid.Column="1" Grid.Row="0" Text="{Binding Path=StartTime}" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
And the code-behind file for the example looks like this
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ProjectPage : ContentPage
{
public ProjectPage()
{
InitializeComponent();
BindingContext = new ProjectsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
Acr.UserDialogs.UserDialogs.Instance.ShowLoading();
var projects = Newtonsoft.Json.JsonConvert.DeserializeObject<IList<ProjectDto>>("[{\"ProjectName\":\"Test sag\",\"ProjectReference\":\"10072\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 3\",\"StartDate\":\"2017-02-02T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"aaa\",\"ProjectReference\":\"10077\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 12\",\"StartDate\":\"2017-02-08T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10082\",\"CustomerName\":\"Test firma\",\"FullAddress\":\"Testvej 50\",\"StartDate\":\"2017-02-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10085\",\"CustomerName\":\"Testvej boligselskab\",\"FullAddress\":\"Testvej 14\",\"StartDate\":\"2017-02-24T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10086\",\"CustomerName\":\"Testing\",\"FullAddress\":\"Testevej 14\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test1\",\"ProjectReference\":\"10087\",\"CustomerName\":\"Plejecenter testlyst\",\"FullAddress\":\"Testlystvej 11\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test2\",\"ProjectReference\":\"10088\",\"CustomerName\":\"Charlie\",\"FullAddress\":\"Testvej 50\",\"StartDate\":\"2017-02-27T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10089\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10090\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10091\",\"CustomerName\":\"Standard Debitor\",\"FullAddress\":\"[Mangler]\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10092\",\"CustomerName\":\"Tester\",\"FullAddress\":\"Testvej 11\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10093\",\"CustomerName\":\"Plejehjemmet test\",\"FullAddress\":\"Testvej 90\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"},{\"ProjectName\":\"Test\",\"ProjectReference\":\"10094\",\"CustomerName\":\"Plejehjemmet test\",\"FullAddress\":\"Testvej 90\",\"StartDate\":\"2017-03-16T00:00:00\",\"StartTime\":\"\"}]");
var viewModel = BindingContext as ProjectsViewModel;
if (viewModel != null)
viewModel.OriginalProjects = projects;
Acr.UserDialogs.UserDialogs.Instance.ShowLoading("Loading");
Task.Delay(5000).ContinueWith((x) =>
{
Device.BeginInvokeOnMainThread(Acr.UserDialogs.UserDialogs.Instance.HideLoading);
Search();
});
}
private void Search(string inputVal = null)
{
var viewModel = BindingContext as ProjectsViewModel;
if (viewModel != null)
{
var projects = viewModel.OriginalProjects.Where(p => !string.IsNullOrEmpty(inputVal) ? p.ProjectName.Contains(inputVal) : true);
var orderedProjects = projects.OrderBy(p => p.StartDate);
Device.BeginInvokeOnMainThread(() =>
{
foreach (ProjectDto project in orderedProjects)
{
var coll = viewModel.Projects.FirstOrDefault(c => c.Key == project.StartDate);
if (coll == null)
viewModel.Projects.Add(coll = new ObservableCollectionWithDateKey { Key = project.StartDate });
coll.Add(project);
}
var group = viewModel.Projects.LastOrDefault();
if (group != null)
ProjectsListView.ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false);
});
}
}
}
class ProjectsViewModel : INotifyPropertyChanged
{
private ObservableCollection<ObservableCollectionWithDateKey> _projects;
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<ProjectDto> OriginalProjects { get; set; }
public ObservableCollection<ObservableCollectionWithDateKey> Projects
{
get { return _projects; }
set
{
_projects = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Projects)));
}
}
public ProjectsViewModel()
{
Projects = new ObservableCollection<ObservableCollectionWithDateKey>();
}
}
public class ProjectDto : INotifyPropertyChanged
{
public string ProjectName { get; set; }
public string ProjectReference { get; set; }
public string CustomerName { get; set; }
public string FullAddress { get; set; }
public DateTime StartDate { get; set; }
public string StartTime { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
class ObservableCollectionWithDateKey : ObservableCollection<ProjectDto>
{
public DateTime Key { get; set; }
}
I use Task.Delay(5000) to simulate a response from the server, but I do not think, it matters.
UPDATE
I figured out, the problem was in my ScrollTo-call, where ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false); was called with the Key instead of just the group.
If you create the grouping first (without adding it to the ViewModel), you have to find the correct model in the ViewModel afterwards. As it otherwise does not find the correct ObservableCollection
I have tested your code and reproduced your issue. The problem is you have passed the wrong parameter to ScrollTo method.
ProjectsListView.ScrollTo(group.First(), group.Key, ScrollToPosition.Start, false);
The group parameter of ScrollTo method is the group from your ListView.ItemsSource. But your passed a group.Key. So the method will not be excited as expect. Please modify the code like following.
Device.BeginInvokeOnMainThread(() =>
{
foreach (ProjectDto project in orderedProjects)
{
var coll = viewModel.Projects.FirstOrDefault(c => c.Key == project.StartDate);
if (coll == null)
viewModel.Projects.Add(coll = new ObservableCollectionWithDateKey { Key = project.StartDate });
coll.Add(project);
}
var group = viewModel.Projects.Last();
if (group != null)
ProjectsListView.ScrollTo(group.First(), group, ScrollToPosition.Start, false);
});
Related
i have a CollectionView and my custom buttom. I want to make a grid with buttons.When I click on button it change a background color.I want to write in void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e) something AND that the text of label is Name(class field) of SELECTED button.When I click on buttons in collectionview it change color but button is not clickable,it does not see it,if I write image it can read data.Please help me make button clickable
<StackLayout>
<Label x:Name="meow1"></Label>
<CollectionView ItemsSource="{Binding Cars}" x:Name="phonesList"
HeightRequest="90"
ItemsLayout="HorizontalList"
BackgroundColor="Transparent"
SelectionMode="Single"
SelectionChanged="OnCollectionViewSelectionChanged">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame x:Name="frame" CornerRadius="10" BackgroundColor="Black" Padding="0" HeightRequest="90"
WidthRequest="95">
<Grid Padding="0" x:Name="meow">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<controls:CustomButton TintColor="#725762" HeightRequest="90"
WidthRequest="90" CornerRadius="10" HorizontalOptions="Center"
BackgroundColor="White" ImageSource="{Binding ImagePath}" Clicked="Button_OnClicked"/>
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
meow1.Text = (e.CurrentSelection.FirstOrDefault() as Car).NameImage;
}
Although not too much understanding the problem , but there is a suggestion about Button click event in CollectionView. We will use Command and CommandParameter of Button when binding model . And that is the design idea of MVVM.
For example , the Xaml code modeified as follow:
<StackLayout>
<Label x:Name="meow1"
Text="{Binding SelectedCarItem.NameImage}"
FontSize="Large"
VerticalOptions="Start"
HorizontalOptions="CenterAndExpand" />
<CollectionView ItemsSource="{Binding Cars}"
x:Name="phonesList"
HeightRequest="90"
ItemsLayout="HorizontalList"
BackgroundColor="Transparent"
SelectionMode="Single"
SelectedItem="{Binding SelectedCarItem}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame x:Name="frame"
CornerRadius="10"
BackgroundColor="{Binding BgFrameColor}"
Padding="0"
HeightRequest="90"
WidthRequest="95">
<Grid Padding="0"
x:Name="meow">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
HeightRequest="90"
WidthRequest="90"
CornerRadius="10"
HorizontalOptions="Center"
BackgroundColor="{Binding BgButtonColor}"
ImageSource="{Binding ImagePath}"
Command="{Binding TapCommand}"
CommandParameter="{Binding Source={x:Reference frame}, Path=BindingContext}" />
</Grid>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
Then need to modify the Car model , adding BgColor,IsSelected and TapCommand property:
public class Car : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string NameImage { get; set; }
public string ImagePath { get; set; }
private Color bgFrameColor;
public Color BgFrameColor
{
set
{
if (bgFrameColor != value)
{
bgFrameColor = value;
OnPropertyChanged("BgFrameColor");
}
}
get
{
return bgFrameColor;
}
}
private Color bgButtonColor;
public Color BgButtonColor
{
set
{
if (bgButtonColor != value)
{
bgButtonColor = value;
OnPropertyChanged("BgButtonColor");
}
}
get
{
return bgButtonColor;
}
}
private bool isSelected;
public bool IsSelected
{
set
{
if (isSelected != value)
{
isSelected = value;
OnPropertyChanged("IsSelected");
}
}
get
{
return isSelected;
}
}
public ICommand TapCommand
{
get
{
return new Command((e) =>
{
var item = (e as Car);
// logic on item
if (item.isSelected)
{
item.isSelected = false;
item.BgButtonColor = Color.White;
item.BgFrameColor = Color.Black;
PageCollectionView.SelectedCar.Remove(item);
MessagingCenter.Send<object, Car>(this, "Hi", new Car() {NameImage ="Welcome to the car home!" });
}
else
{
item.isSelected = true;
item.BgButtonColor = Color.Blue;
item.BgFrameColor = Color.Yellow;
if (PageCollectionView.SelectedCar.Count == 0)
{
PageCollectionView.SelectedCar.Add(item);
}
else
{
PageCollectionView.SelectedCar[0].isSelected = false;
PageCollectionView.SelectedCar[0].BgButtonColor = Color.White;
PageCollectionView.SelectedCar[0].BgFrameColor = Color.Black;
PageCollectionView.SelectedCar.Remove(PageCollectionView.SelectedCar[0]);
PageCollectionView.SelectedCar.Add(item);
}
MessagingCenter.Send<object, Car>(this, "Hi", item);
}
});
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Add CarModel class to load data :
public class CarModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public List<Car> Cars { get; set; }
//public static Car SelectedCarItem { set; get; }
public CarModel()
{
Cars = new List<Car>();
Cars.Add(new Car() { NameImage = "Lexus", ImagePath = "Lexus.png", BgButtonColor = Color.White, BgFrameColor = Color.Black, IsSelected = false }); ;
Cars.Add(new Car { NameImage = "Audi", ImagePath = "Audi.png", BgButtonColor = Color.White, BgFrameColor = Color.Black, IsSelected = false });
// set default text of label
selectedCarItem = new Car() { NameImage = "Welcome to the car home!" };
}
private Car selectedCarItem;
public Car SelectedCarItem
{
get
{
return selectedCarItem;
}
set
{
if (selectedCarItem != value)
{
selectedCarItem = value;
OnPropertyChanged("SelectedCarItem");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now in ContentPage , declare a List<Car> to store only one item to keep this collection view is single selected. And use MessagingCenter to update carModel.SelectedCarItem here:
public partial class PageCollectionView : ContentPage
{
public static List<Car> SelectedCar { get; set; }
public PageCollectionView()
{
InitializeComponent();
CarModel carModel = new CarModel();
BindingContext = carModel;
SelectedCar = new List<Car>();
MessagingCenter.Subscribe<object,Car>(this, "Hi", (sender,arg) =>
{
// Do something whenever the "Hi" message is received
carModel.SelectedCarItem = arg;
});
}
}
The effect as follow :
Note: From the sample , you will see that using binding to modify BackgroundColor and Model Data. Therefore, it's not recommanded to use OnCollectionViewSelectionChanged to modify text of Lable .
I have already created all of the controls, and have tested that the controls are added as needed without using the binding.
The issue is as soon as I add the binding which is supposed to determine which control to add, the controls stop working as needed.
The list view control will be populated from a collection of a class, which will have an indicator field to determine which control needs to be loaded. The list view contains a 2nd user control which basically acts as a placeholder for the correct control, it has a bindable property of type text which is set to determine the correct control to be loaded.
Here is the XAML Code for the list view control
<ContentView.Content>
<StackLayout>
<Label Text="Binding Control Type"/>
<Entry x:Name="cntName"/>
<ListView x:Name="GroupedView" GroupDisplayBinding="{Binding Title}" HasUnevenRows="True" GroupShortNameBinding="{Binding ShortName}" IsGroupingEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Add Comment"/>
<MenuItem Text="Add Attachment"/>
</ViewCell.ContextActions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="7*"/>
</Grid.RowDefinitions>
<Label Text="{Binding QUESTION_ID}" Grid.Row="0" Grid.Column="0" VerticalTextAlignment="Center" FontSize="Medium"/>
<Label Text="{Binding QUESTION_DETAILS}" Grid.Row="1" Grid.Column="0" VerticalTextAlignment="Center" FontSize="Medium"/>
<con:ucListViewControls ControlType="{Binding QUESTION_ANSWERCONTROL}" Grid.Row="1" Grid.Column="1"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem Text="Add Comment"/>
<MenuItem Text="Add Attachment"/>
</ViewCell.ContextActions>
<StackLayout Orientation="Horizontal" Padding="5,5,5,5" BackgroundColor="#E2F5F9">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={x:Reference this}, Path=Tapped}" CommandParameter="{Binding .}"/>
</StackLayout.GestureRecognizers>
<Button Image="{Binding StateIcon}" BackgroundColor="Transparent" BorderColor="Transparent" BorderWidth="0"/>
<Label Text="{Binding Title}" TextColor="#005569" FontSize="15" VerticalOptions="Center"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
And the code behind for the control
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ucExpandibleListView : ContentView
{
private ObservableCollection<dbQuestionGroup> _allGroups;
private ObservableCollection<dbQuestionGroup> _expandedGroups;
public ucExpandibleListView()
{
InitializeComponent();
Tapped = new Command(x => HeaderTapped(x));
_allGroups = new ObservableCollection<dbQuestionGroup>()
{
new dbQuestionGroup("Category 1", "C1", false)
{
new dbQuestionModel() { QUESTION_ID = 1, QUESTION_DETAILS = "Testing Question 1", QUESTION_ANSWERCONTROL = "RBL" },
new dbQuestionModel() { QUESTION_ID = 2, QUESTION_DETAILS = "Testing Question 2", QUESTION_ANSWERCONTROL = "" }
}
};
UpdateListContent();
}
private void UpdateListContent()
{
_expandedGroups = new ObservableCollection<dbQuestionGroup>();
foreach (dbQuestionGroup group in _allGroups)
{
dbQuestionGroup newGroup = new dbQuestionGroup(group.Title, group.ShortName, group.Expanded);
newGroup.QuestionCount = group.Count;
if (group.Expanded)
{
foreach (dbQuestionModel question in group)
{
newGroup.Add(question);
}
}
_expandedGroups.Add(newGroup);
}
GroupedView.ItemsSource = _expandedGroups;
}
public Command Tapped { get; set; }
private void HeaderTapped(object group)
{
var groupCat = (dbQuestionGroup)group;
int selectedIndex = _expandedGroups.IndexOf(groupCat);
if (groupCat.Expanded)
{
_allGroups[selectedIndex].Expanded = false;
}
else
{
_allGroups.ToList().ForEach(x => x.Expanded = false);
_allGroups[selectedIndex].Expanded = !_allGroups[selectedIndex].Expanded;
}
UpdateListContent();
}
}
Here is the XAML Code for the placeholder control
<ContentView.Content>
<StackLayout x:Name="stkPlaceholder">
</StackLayout>
</ContentView.Content>
And the code behind for the placeholder control
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ucListViewControls : ContentView, INotifyPropertyChanged
{
public ucListViewControls()
{
InitializeComponent();
}
#region Control Attributes
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string info)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
}
#endregion
#region Bindable Properties
public static readonly BindableProperty ControlTypeProperty = BindableProperty.Create(nameof(ControlType), typeof(string), typeof(ucListViewControls));
public string ControlType
{
get
{
return (string)GetValue(ControlTypeProperty);
}
set
{
SetValue(ControlTypeProperty, value);
AddControl();
NotifyPropertyChanged("ControlType");
}
}
#endregion
public void AddControl()
{
switch (ControlType)
{
case "RBL":
ucRadiobuttons radiobuttons = new ucRadiobuttons();
radiobuttons.lblTitle1 = "Yes";
radiobuttons.lblTitle2 = "No";
radiobuttons.lblTitle3 = "N/A";
radiobuttons.OnColor1 = Color.Green;
radiobuttons.OnColor2 = Color.Red;
radiobuttons.OnColor3 = Color.Transparent;
stkPlaceholder.Children.Add(radiobuttons);
break;
default:
Entry placeholder = new Entry();
stkPlaceholder.Children.Add(placeholder);
break;
}
}
}
I have tested that the controls are added without the binding, which works perfectly.
I have tried to rewrite the bindable property multiple times in case I missed something, I also could not find any post relating to something similar which wouldve helped me.
Any clues?
The first screenshot shows the expected output, and the seconds screenshots shows what happens when the binding is applied.
dbQuestionModel:
using System.Collections.Generic;
namespace PivotMobile_BusinessLayer.Models
{
public class dbQuestionModel
{
public int QUESTION_PK { get; set; }
public int QUESTION_ID { get; set; }
public string QUESTION_CATEGORY { get; set; }
public string QUESTION_DETAILS { get; set; }
public string QUESTION_TYPE { get; set; }
public string QUESTION_ANSWERCONTROL { get; set; }
public string QUESTION_COMMENT { get; set; }
public List<string> QUESTION_ATTACHMENTS { get; set; }
}
}
dbQuestionGroup:
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace PivotMobile_BusinessLayer.Models
{
public class dbQuestionGroup : ObservableCollection<dbQuestionModel>, INotifyPropertyChanged
{
public static ObservableCollection<dbQuestionGroup> All { private set; get; }
private bool _expanded;
public string Title { get; set; }
public string ShortName { get; set; }
public bool Expanded
{
get
{
return _expanded;
}
set
{
if (_expanded != value)
{
_expanded = value;
OnPropertyChanged("Expanded");
OnPropertyChanged("StateIcon");
}
}
}
public string StateIcon
{
get
{
return Expanded ? "expanded_blue.png" : "collapsed_blue.png";
}
}
public int QuestionCount { get; set; }
public dbQuestionGroup(string title, string shortName, bool expanded = true)
{
Title = title;
ShortName = shortName;
Expanded = expanded;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ListView Page XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PivotMobile.Views.ObservationsView"
xmlns:con="clr-namespace:PivotMobile.Controls">
<ContentPage.Content>
<StackLayout Margin="5">
<con:ucExpandibleListView/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
ListView Page Code Behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PivotMobile.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ObservationsView : ContentPage
{
public ObservationsView ()
{
InitializeComponent ();
}
}
}
I have a problem. What I am trying to do is bind an ImageSource from a ViewModel. The image file name is called ledstrip.png and is located in the Resources/Drawable folder. I am using the following xaml:
<ListView ItemsSource="{Binding knownDeviceList}" SelectionMode="None" RowHeight="90">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<AbsoluteLayout HeightRequest="70" Margin="20,20,20,0">
<StackLayout Opacity="0.3" BackgroundColor="White"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All" />
<StackLayout AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="35" />
<RowDefinition Height="35" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>
<Image Source="{Binding DeviceImage}" Grid.RowSpan="2" Grid.Column="0" Margin="5" />
</Grid>
</StackLayout>
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And in my ViewModel I have the following code:
public class VM_DeviceList : BindableObject
{
private ObservableCollection<DisplayedDevice> _knownDeviceList;
public ObservableCollection<DisplayedDevice> knownDeviceList
{
get
{
return _knownDeviceList;
}
set
{
if (_knownDeviceList != value)
{
_knownDeviceList = value;
OnPropertyChanged();
}
}
}
public VM_DeviceList()
{
knownDeviceList = new ObservableCollection<DisplayedDevice>();
MyHandler();
}
private async Task LoadKnownDeviceList()
{
foreach (KnownDevice device in App.KnownDeviceList)
{
DisplayedDevice displayedDevice = new DisplayedDevice();
displayedDevice.Id = device.Id;
displayedDevice.Name = device.Name;
switch (device.Type)
{
case "ledstrip":
displayedDevice.DeviceImage = "ledstrip.png";
break;
case "triangle":
displayedDevice.DeviceImage = "triangle.png";
break;
}
knownDeviceList.Add(displayedDevice);
}
}
public Task MyHandler()
{
return LoadKnownDeviceList();
}
public class DisplayedDevice
{
public int Id { get; set; }
public string Name { get; set; }
public string DeviceImage { get; set; }
}
}
The problem is that when I type "ledstrip.png" in the xaml ImageSource, the image gets displayed, but when I bind it like the way I show above, no image appears on the screen!
What am I doing wrong and how can I fix this?
Since you are altering the DisplayImage on a separate for loop. You have to Notify the UI that the DisplayImage property value has been changed.
Use INotifyPropertyChanged for notifying that DisplayImage property of DisplayedDevice class has been changed to the UI.
public class DisplayedDevice : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string deviceImage;
public int Id { get; set; }
public string Name { get; set; }
public string DeviceImage
{
get
{
return deviceImage;
}
set
{
deviceImage = value;
OnPropertyChanged();
}
}
}
I have a DataGrid which is bound to a ViewModel. When I select a record from the DataGrid, the TextBoxes (Username and Role) are displaying the data from the selected record.
I want to edit the selected record but I'd like to check the data before it updates the list, hence the 'OneWay' binding mode.
I'm having trouble passing the values of the textboxes to the view model. I can get a value of one textboxes through the button and passing the value to my ICommand
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}
Is there a way to pass all the textboxes to the view model by creating a property in it that holds selected user? or passing the values of the texboxes to the view model somehow??
Thanks.
My view model
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private ICommand addUserCommand;
private ICommand removeUserCommand;
private ICommand editUserCommand;
public ICommand AddUserCmd => addUserCommand ?? (addUserCommand = new AddUserCommand(this));
public ICommand RemoveUserCmd => removeUserCommand ?? (removeUserCommand = new DeleteUserCommand(this));
public ICommand EditUserCmd => editUserCommand ?? (editUserCommand = new EditUserCommand(this));
private UsersModel selectedUser = new UsersModel();
public UsersModel SelectedUser
{
get { return this.selectedUser; }
set
{
this.selectedUser = value;
}
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
Model
public class UsersModel
{
public int Id { get; set; }
public string Username { get; set; }
public string Surname {get; set;}
}
Edit Command
internal class EditUserCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public UsersViewModel UsersViewModel { get; set; }
public EditUserCommand(UsersViewModel usersViewModel)
{
this.UsersViewModel = usersViewModel;
}
public bool CanExecute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
//return !string.IsNullOrEmpty(user.Id.ToString());
return true;
}
public void Execute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
// this.UsersViewModel.Users
}
}
xaml
...
<Window.Resources>[enter image description here][1]
<m:UsersModel x:Key="users"></m:UsersModel>
<vm:UsersViewModel x:Key="viewModelUsers"/>
</Windows.Resources>
...
<DataGrid x:Name="gridUsers"
Grid.Row="0"
DataContext="{Binding Source={StaticResource viewModelUsers}}" CanUserAddRows="False"
ItemsSource="{Binding Users}">
</DataGrid>
<Grid Margin="10" Grid.Row="1" DataContext="{Binding ElementName=gridUsers, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Username, Mode=OneWay}"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Role, Mode=OneWay}"/>
<StackPanel Grid.Row="5" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}">
</StackPanel>
</Grid>
Your ViewModel should not know about text boxes - just add two new properties ([PropertyName]EditValue) and bind to them, then in your command check them and copy them to the model if correct or restore them if incorrect - This is the entire point in using view models instead of binding to models directly
Did you know that you can edit the DataGrid cells directly? You can even use data validation. This way invalid data cells get a red border and the data won't be committed unless the validation passes.
Another option is to let UsersModel implement INotifyDataErrorInfo and validate properties directly. Then bind the DataGrid.SelectedItem to the view model and bind the edit TextBox elements to this property. This way you implemented live update and got rid of the edit commands:
UsersViewModel.cs
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private UsersModel selectedUser;
public UsersModel SelectedUser
{
get => this.selectedUser;
set => this.selectedUser = value;
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
UsersModel.cs
public class UsersModel : INotifyDataErrorInfo
{
private int id;
public int Id
{
get => this.id;
set { if (this.id != value && IsIdValid(value)) this.id = value; }
}
private string userName;
public string UserName
{
get => this.userName;
set { if (this.userName != value && IsUserNameValid(value) && ) this.userName = value; }
}
private string surname;
public string Surname
{
get => this.surname;
set { if (this.surname != value && IsSurnameValid(value) && ) this.surname = value; }
}
// Validates the Id property, updating the errors collection as needed.
public bool IsIdValid(int value)
{
RemoveError(nameof(this.Id), ID_ERROR);
if (value < 0)
{
AddError(nameof(this.Id), ID_ERROR, false);
return false;
}
return true;
}
public bool IsUserNameValid(string value)
{
RemoveError(nameof(this.UserName), USER_NAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.UserName), USER_NAME_ERROR, false);
return false;
}
return true;
}
public bool IsSurnameValid(string value)
{
RemoveError(nameof(this.Surname), SURNAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.Surname), SURNAME_ERROR, false);
return false;
}
return true;
}
private Dictionary<String, List<String>> errors =
new Dictionary<string, List<string>>();
private const string ID_ERROR = "Value cannot be less than 0.";
private const string USER_NAME_ERROR = "Value cannot be empty.";
private const string SURNAME_ERROR = "Value cannot be empty.";
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!errors.ContainsKey(propertyName))
errors[propertyName] = new List<string>();
if (!errors[propertyName].Contains(error))
{
if (isWarning) errors[propertyName].Add(error);
else errors[propertyName].Insert(0, error);
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (errors.ContainsKey(propertyName) &&
errors[propertyName].Contains(error))
{
errors[propertyName].Remove(error);
if (errors[propertyName].Count == 0) errors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
public void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!errors.ContainsKey(propertyName)) return null;
return errors[propertyName];
}
public bool HasErrors
{
get => errors.Count > 0;
}
#endregion
}
View
TextBox.Text bindings must be set to TwoWay (which is the default Binding.Mode value for this property)
<DataGrid x:Name="gridUsers"
DataContext="{Binding Source={StaticResource viewModelUsers}}"
CanUserAddRows="False"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser"} />
<Grid DataContext="{Binding Source={StaticResource viewModelUsers}, Path=SelectedUser}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1"
Text="{Binding Username, NotifyOnValidationError=True"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1"
Text="{Binding Role, NotifyOnValidationError=True}"/>
</Grid>
I am dealing with a listview in xamarin.froms. I can easily populate the listview with a listitem for each record:
[
{"cat":1, "name":"alpha"},
{"cat":1, "name":"beta"},
{"cat":1, "name":"gamma"},
{"cat":2, "name":"john"},
{"cat":2, "name":"william"},
{"cat":2, "name":"smith"},
{"cat":2, "name":"steve"},
{"cat":3, "name":"abc"},
{"cat":3, "name":"xyz"}
]
//9 Items in listview from this json source
But what I want is to group all the items on some key value, say "cat" here and achieve something like this:
Any suggestion or approach toward this would be appreciated.
Group your data into collections and add those collections into your ListView. The collections need to be your own class with a property for binding the group with.
Here's a walkthrough:
Set up grouping on your ListView including a property to bind each group to, in this case "GroupKey"
myListView.IsGroupingEnabled = true;
myListView.GroupDisplayBinding = new Binding("GroupKey"); // See below
And then add your data in groups (e.g. lists of lists). This often means you need to create your own class to show your groupings, such as:
public class Grouping<K, T> : ObservableCollection<T>
{
// NB: This is the GroupDisplayBinding above for displaying the header
public K GroupKey { get; private set; }
public Grouping(K key, IEnumerable<T> items)ac
{
GroupKey = key;
foreach (var item in items)
this.Items.Add(item);
}
}
And finally, add your data in groups:
var groups = new ObservableCollection<Grouping<string, MyDataClass>>();
// You can just pass a set of data in (where "GroupA" is an enumerable set)
groups.Add(new Grouping<string, MyDataClass>("GroupA", GroupA));
// Or filter down a set of data
groups.Add(new Grouping<string, MyDataClass>("GroupB",
MyItems.Where(a => a.SomeFilter())));
myListView.ItemSource = groups;
Bind your cell to the MyDataClass as you would have before:
var cell = new DataTemplate(typeof(TextCell));
cell.SetBinding(TextCell.TextProperty, "SomePropertyFromMyDataClass");
cell.SetBinding(TextCell.DetailProperty, "OtherPropertyFromMyDataClass");
myListView.ItemTemplate = cell;
Check it out for explanation on why to use template K instead of a string in the Grouping class, how to customise the header look, and much more:
http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping
(Credit to the link in #pnavk's answer)
In my environment, the code was not work in this url http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping .
The code that does not work is here
var partnersSorted = from item in Partners
orderby item.UserName
group item by item.UserNameSort into PartnersGroup
select new Grouping<string, Monkey>(PartnersGroup.Key, PartnersGroup);
MonkeysGrouped = new ObservableCollection<Grouping<string, Monkey>>(partnersSorted);
So I changed the code.
var sortedPartners = Partners.OrderBy(x => x.UserName).GroupBy(y => y.UserNameSort);
foreach (var item in sortedPartners)
{
PartnersGrouped.Add(new PartnersGrouping<string, Item>(item.Key, Partners.Where(x=>x.UserNameSort == item.Key)));
}
You can see like this.
[https://i.stack.imgur.com/BswPq.png][1]
here is my all scripts
Item is Partner
Item.cs
using System;
namespace NewHeats.Models
{
public class Item
{
public string Id
{
get;
set;
}
public string UserName
{
get;
set;
}
public DateTime RegisterDate
{
get;
set;
}
public string Field
{
get;
set;
}
public string Password
{
get;
set;
}
public int Heats
{
get;
set;
}
public string UserNameSort
{
get
{
if (string.IsNullOrWhiteSpace(UserName) || UserName.Length == 0)
return "?";
return UserName[0].ToString().ToUpper();
}
}
}
}
PartnersGrouping.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace NewHeats.Models
{
public class PartnersGrouping<K,T> : ObservableCollection<T>
{
public K Key { get; private set; }
public PartnersGrouping(K key,IEnumerable<T> items)
{
Key = key;
foreach (var item in items)
{
this.Items.Add(item);
}
}
}
}
PartnersViewModel.cs
using System;
using System.Windows.Input;
using System.ComponentModel;
using System.Collections.ObjectModel;
using Xamarin.Forms;
using NewHeats.Models;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Linq;
using System.Diagnostics.Contracts;
namespace NewHeats.ViewModels
{
public class PartnersViewModel : BaseViewModel
{
public Item Me
{
get;
set;
}
public ObservableCollection<Item> Partners { get; set; }
public ObservableCollection<PartnersGrouping<string, Item>> PartnersGrouped { get; set; }
public Item SelectedPartner { get; set; }
public Command LoadPartnersCommand { get; set; }
public PartnersViewModel()
{
Title = "Partners";
Partners = new ObservableCollection<Item>();
PartnersGrouped = new ObservableCollection<PartnersGrouping<string, Item>>();
LoadPartnersCommand = new Command(async() =>await ExecuteLoadPartnersCommand());
}
async Task ExecuteLoadPartnersCommand()
{
Contract.Ensures(Contract.Result<Task>() != null);
if (IsBusy)
return;
IsBusy = true;
try
{
Me = await MockUsrDataStore.GetItemAsync("naoto");
Partners.Clear();
var allfriends = await MockFriDataStore.GetItemsAsync(true);
var myFriends = allfriends.Where(x => x.MyId == Me.Id);
var allUsers = await MockUsrDataStore.GetItemsAsync(true);
foreach (var item in myFriends)
{
var partner = allUsers.FirstOrDefault(x => x.Id == item.FriendId);
if (partner!=null)
{
Partners.Add(partner);
}
}
var sortedpartners = Partners.OrderBy(x => x.UserName).GroupBy(y => y.UserNameSort);
foreach (var item in sortedpartners)
{
PartnersGrouped.Add(new PartnersGrouping<string, Item>(item.Key, Partners.Where(x=>x.UserNameSort == item.Key)));
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
}
PartnerPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="NewHeats.Views.PartnersPage" xmlns:vm="clr-namespace:NewHeats.ViewModels" xmlns:controls="clr-namespace:ImageCircle.Forms.Plugin.Abstractions;assembly=ImageCircle.Forms.Plugin" Title="{Binding Title}">
<ContentPage.Resources>
<ResourceDictionary>
<!--Page Level Resources: Compatibile with Xamarin Live Player -->
<Color x:Key="Primary">#2196F3</Color>
<Color x:Key="Accent">#96d1ff</Color>
<Color x:Key="LightTextColor">#999999</Color>
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollView Grid.Row="0">
<StackLayout Orientation="Vertical" Padding="16,40,16,40" Spacing="10">
<ListView ItemsSource="{Binding PartnersGrouped}"
HasUnevenRows="true"
VerticalOptions="FillAndExpand"
IsPullToRefreshEnabled="true"
CachingStrategy="RecycleElement"
IsRefreshing="{Binding IsBusy, Mode=OneWay}"
RefreshCommand="{Binding LoadPartnersCommand}"
ItemSelected="Handle_ItemSelected"
SelectedItem="{Binding SelectedPartner}"
GroupDisplayBinding="{Binding Key}"
IsGroupingEnabled="true"
GroupShortNameBinding="{Binding Key}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell Height="25">
<StackLayout VerticalOptions="FillAndExpand"
Padding="5"
BackgroundColor="#3498DB">
<Label Text="{Binding Key}" TextColor="White" VerticalOptions="Center"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<controls:CircleImage Source="husky.jpg"
Aspect="AspectFill"
Grid.Column="0"
Grid.Row="0"
WidthRequest="60"
HeightRequest="60">
</controls:CircleImage>
<StackLayout Orientation="Vertical" Grid.Column="1">
<Label Text="{Binding UserName}" VerticalTextAlignment="Center"/>
</StackLayout>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ScrollView>
</Grid>
</ContentPage>
Thanks!!
Xamarin.Forms ListView does support grouping. Please review their documentation http://developer.xamarin.com/guides/cross-platform/xamarin-forms/user-interface/listview/customizing-list-appearance/#Grouping to see how to use it.