I have a problem with the Picker control in .NET MAUI. On the update page, the picker is not showing the value of update model.
Here is how is picker defined in the xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiUI.Pages.AddOrUpdatePlayer"
xmlns:local="clr-namespace:Backend.Models;assembly=Backend.Models"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<ContentPage.BindingContext>
<local:PlayerModel x:Name="ViewModel"/>
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem IconImageSource="save.svg" Clicked="OnSaveClick" Command="{Binding ValidateCommand}">
</ToolbarItem>
</ContentPage.ToolbarItems>
<ScrollView Margin="10">
<VerticalStackLayout>
<VerticalStackLayout>
<Label Text="Name" />
<Entry x:Name="name" Text="{Binding Name}"
ClearButtonVisibility="WhileEditing">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Name].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorName" Text="{Binding [Name].Error}" TextColor="Red" />
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Position" />
<Picker x:Name="position" Title="Select..."
ItemDisplayBinding="{Binding Name}"
SelectedItem="{Binding Position}">
</Picker>
<Label x:Name="lblValidationErrorPosition" TextColor="red" Text="{Binding [Position].Error}"/>
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Club" />
<Entry x:Name="club" Text="{Binding Club}"
ClearButtonVisibility="WhileEditing">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Club].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorClub" TextColor="red" Text="{Binding [Club].Error}"/>
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Birthday" />
<DatePicker x:Name="birthday" Date="{Binding Birthday}"/>
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Birth place" />
<Entry x:Name="birthplace" Text="{Binding BirthPlace}"
ClearButtonVisibility="WhileEditing">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [BirthPlace].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorBirthPlace" TextColor="red" Text="{Binding [BirthPlace].Error}"/>
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Weight" />
<Entry x:Name="weight" Text="{Binding Weight}"
ClearButtonVisibility="WhileEditing" Keyboard="Numeric">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Weight].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorWeight" TextColor="red" Text="{Binding [Weight].Error}" />
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Height" />
<Entry x:Name="height" Text="{Binding Height}"
ClearButtonVisibility="WhileEditing" Keyboard="Numeric">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Height].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorHeight" TextColor="red" Text="{Binding [Height].Error}" />
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Image link" />
<Entry x:Name="webImageLink" Text="{Binding WebImageLink}"
ClearButtonVisibility="WhileEditing">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [WebImageLink].HasError}" />
</Entry.Behaviors>
</Entry>
<Label x:Name="lblValidationErrorWebImageLink" TextColor="red" Text="{Binding [WebImageLink].Error}"/>
</VerticalStackLayout>
<VerticalStackLayout Margin="0,10">
<Label Text="Description" />
<Editor x:Name="description" Text="{Binding Description}"
AutoSize="TextChanges">
<Editor.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Description].HasError}" />
</Editor.Behaviors>
</Editor>
<Label x:Name="lblValidationErrorDescription" TextColor="red" Text="{Binding [Description].Error}"/>
</VerticalStackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
The code behind:
[QueryProperty(nameof(Player), "player")]
public partial class AddOrUpdatePlayer : ContentPage
{
private PlayerModel player;
public PlayerModel Player
{
get => player;
set
{
player = value;
OnPropertyChanged("player");
}
}
private readonly IMemoryCache memoryCache;
private readonly IPlayerClient playerClient;
private delegate Task Action();
private Action asyncAction;
public AddOrUpdatePlayer(IMemoryCache memoryCache, IPlayerClient playerClient)
{
this.memoryCache = memoryCache;
this.playerClient = playerClient;
InitializeComponent();
SetUpPositionPicker();
}
protected override void OnAppearing()
{
player ??= new PlayerModel();
player.ValidationCompleted += OnValidationHandler;
BindingContext = player;
SetUpControls();
SetTitle();
SetActionPointer();
}
private void SetUpControls()
{
birthday.MinimumDate = new DateTime(1900, 1, 1);
birthday.MaximumDate = DateTime.Now.Date;
memoryCache.TryGetValue(CacheKeys.Positions, out List<PositionModel> positions);
var selectedPosition = positions.FirstOrDefault(x => x.Id == player?.Position?.Id);
var index = positions.IndexOf(selectedPosition);
position.SelectedIndex = index;
}
private void SetUpPositionPicker()
{
memoryCache.TryGetValue(CacheKeys.Positions, out List<PositionModel> positions);
position.ItemsSource = positions;
}
private void SetTitle()
{
Title = this.player?.Id == 0 ?
"Add new player" :
$"Update {player?.Name}";
}
private void SetActionPointer()
{
asyncAction = this.player?.Id == 0 ?
AddNewPlayer :
UpdatePlayer;
}
private async Task AddNewPlayer()
{
var result = await playerClient.CreateAsync(player);
if (!result)
return;
}
private async Task UpdatePlayer()
{
var result = await playerClient.UpdateAsync(player);
if (!result)
return;
}
private async void OnSaveClick(object sender, EventArgs e)
{
if (player?.HasErrors ?? true)
return;
await asyncAction();
}
private void OnValidationHandler(Dictionary<string, string?> validationMessages)
{
if (validationMessages is null)
return;
lblValidationErrorName.Text = validationMessages.GetValueOrDefault("name");
lblValidationErrorPosition.Text = validationMessages.GetValueOrDefault("positionid");
lblValidationErrorClub.Text = validationMessages.GetValueOrDefault("club");
lblValidationErrorWebImageLink.Text = validationMessages.GetValueOrDefault("webimagelink");
lblValidationErrorBirthPlace.Text = validationMessages.GetValueOrDefault("birthplace");
lblValidationErrorWeight.Text = validationMessages.GetValueOrDefault("weight");
lblValidationErrorHeight.Text = validationMessages.GetValueOrDefault("height");
lblValidationErrorDescription.Text = validationMessages.GetValueOrDefault("description");
}
}
public partial class PlayerModel : BaseViewModel
{
private int id;
private string name;
private string webImageLink;
private string club;
private string birthday;
private string birthPlace;
private int? weight;
private double? height;
private string description;
private PositionModel position;
public int Id
{
get => this.id;
set => SetProperty(ref this.id, value, true);
}
[Required]
[StringLength(255)]
[MinLength(2)]
public string Name
{
get => this.name;
set
{
SetProperty(ref this.name, value, true);
ClearErrors();
SetProperty(ref this.name, value);
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Name]");
}
}
[Required]
[StringLength(4096)]
public string WebImageLink
{
get => this.webImageLink;
set
{
SetProperty(ref this.webImageLink, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[WebImageLink]");
}
}
[Required]
[StringLength(255)]
[MinLength(2)]
public string Club
{
get => this.club;
set
{
SetProperty(ref this.club, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Club]");
}
}
[Required]
[StringLength(32)]
public string Birthday
{
get => this.birthday;
set
{
SetProperty(ref this.birthday, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Birthday]");
}
}
[Required]
[StringLength(255)]
public string BirthPlace
{
get => this.birthPlace;
set
{
SetProperty(ref this.birthPlace, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[BirthPlace]");
}
}
[Required]
[Range(0, 100)]
public int? Weight
{
get => this.weight;
set
{
SetProperty(ref this.weight, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Weight]");
}
}
[Required]
[Range(0, 2.5)]
public double? Height
{
get => this.height;
set
{
SetProperty(ref this.height, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Height]");
}
}
[Required]
public string Description
{
get => this.description;
set
{
SetProperty(ref this.description, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Description]");
}
}
[Required]
public PositionModel Position
{
get => this.position;
set
{
SetProperty(ref this.position, value, true);
ClearErrors();
ValidateAllProperties();
OnPropertyChanged("ErrorDictionary[Name]");
}
}
public PlayerModel() : base()
{}
public PlayerModel(int id, string name, string webImageLink, string club, string birthday, string birthPlace, int weight, double height, string description, string positionName, int positionId) : base()
{
Id = id;
Name = name;
WebImageLink = webImageLink;
Club = club;
Birthday = birthday;
BirthPlace = birthPlace;
Weight = weight;
Height = height;
Description = description;
Position = new PositionModel(positionId, positionName);
}
public PlayerModel(int id, string name, string webImageLink, string club, string birthday, string birthPlace, int weight, double height, string description, PositionModel position) : base()
{
Id = id;
Name = name;
WebImageLink = webImageLink;
Club = club;
Birthday = birthday;
BirthPlace = birthPlace;
Weight = weight;
Height = height;
Description = description;
Position = position;
}
public PlayerModel(PlayerEntity player)
{
Id = player.Id;
Name = player.Name;
WebImageLink = player.WebImageLink;
Club = player.Club;
Birthday = player.Birthday;
BirthPlace = player.BirthPlace;
Weight = player.Weight;
Height = player.Height;
Description = player.Description;
Position = new PositionModel(player.Position);
}
public PlayerEntity ToEntity()
{
return new PlayerEntity
{
Id = Id,
Name = Name,
WebImageLink = WebImageLink,
Club = Club,
Birthday = Birthday,
BirthPlace = BirthPlace,
Weight = Weight.Value,
Height = Height.Value,
Description = Description,
PositionId = Position.Id
};
}
public void ToEntity(PlayerEntity player)
{
player.Id = Id;
player.Name = Name;
player.WebImageLink = WebImageLink;
player.Club = Club;
player.Birthday = Birthday;
player.BirthPlace = BirthPlace;
player.Weight = Weight.Value;
player.Height = Height.Value;
player.Description = Description;
player.PositionId = Position.Id;
}
}
public delegate void NotifyWithValidationMessages(Dictionary<string, string?> validationDictionary);
public class BaseViewModel : ObservableValidator
{
public event NotifyWithValidationMessages? ValidationCompleted;
public virtual ICommand ValidateCommand => new RelayCommand(() =>
{
ClearErrors();
ValidateAllProperties();
var validationMessages = this.GetErrors()
.ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);
ValidationCompleted?.Invoke(validationMessages);
});
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
var errors = this.GetErrors()
.ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();
var hasErrors = errors.TryGetValue(propertyName, out var error);
return new ValidationStatus(hasErrors, error ?? string.Empty);
}
}
public BaseViewModel() : base()
{}
}
public class ValidationStatus : ObservableObject
{
private bool hasError;
private string error;
public bool HasError
{
get => this.hasError;
set => SetProperty(ref this.hasError, value);
}
public string Error
{
get => this.error;
set => SetProperty(ref this.error, value);
}
public ValidationStatus()
{
}
public ValidationStatus(bool hasError, string error)
{
this.hasError = hasError;
this.error = error;
}
}
public class PositionModel
{
[Required]
[Range(1, 7)]
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
public PositionModel()
{
}
public PositionModel(int id, string name)
{
Id = id;
Name = name;
}
public PositionModel(PositionEntity entity)
{
Id = entity.Id;
Name = entity.Name;
}
public PositionEntity ToEntity()
{
return new PositionEntity
{
Id = Id,
Name = Name
};
}
public void ToEntity(PositionEntity entity)
{
entity.Id = Id;
entity.Name = Name;
}
What is very interesting, if I edit the first object everything works as it, but if I navigate on the second object and try to edit it, the position is not set. And so on, the third object will have set the position, the 4th won't, and so on.
Any idea?
At the end, I finally come up with a solution. The Position class has to implement IEquatable interface, then the Picker finds the binded Position object from the ViewModel.
public bool Equals(PositionModel? position) => this.Id == position?.Id &&
this.Name == position?.Name;
Another solution was, as I mentioned in my comment:
,, If we want the Picker to bind to a property of object type, it needs to come from the same collection as the Pickers ItemSource. I have a feeling, that it looks equality on a ,,reference,, type. ,,
UPDATE
Ok, I misunderstood the binding. I think x:Name ViewModel is interfering with BindingContext = player.
Remove BindingContext from xaml.
Do that in constructor instead: BindingContext = new PlayerModel().
Then the later set of BindingContext should work as expected.
———————
Original answer; Wrong explanation
Position is a property on each Player.
Picker is on the page.
<Picker x:Name="position" SelectedItem="{Binding Position}"> is looking for a property Position on the page. Looks like there is no such property.
It won't magically find Position on a player.
I see page has Player property. Assuming that is always the correct player to look at, then bind to Player.Position. I'm not sure what happens if Player is null.
Try:
<Picker x:Name="position" SelectedItem="{Binding Player.Position}">.
Related
I just began working with the MVVM layout and I cannot seem to display anything in my collectionView List. I believe it's my binding, unfortunately I don't really understand how I am supposed to bind a grouped list + the ViewModel. I read to bind to the path no the source, but I'm pretty sure I am doing this incorrectly. I have checked to see if I am even getting shares to load and I am, they're just not displaying.
Model -- Share
[JsonProperty("iSpottedID")]
public int ID { get; set; }
[JsonProperty("sShoppingList")]
[MaxLength(255)]
public string ShoppingName { get; set; }
[JsonProperty("dtInfoUpdate")]
[MaxLength(20)]
public string CreateDate { get; set; }
[JsonProperty("iProductID")]
public int ProductID { get; set; }
[Indexed]
[JsonProperty("sLocation")]
[MaxLength(255)]
public string LocationName { get; set; }
[JsonProperty("tvCardJson")]
public string JsonString { get; set; }
ViewModel -- SharesViewModel
public class SharesViewModel : BaseViewModel
{
#region Properties
private int _id;
public int ID
{
get { return _id; }
set
{
SetValue(ref _id, value);
OnPropertyChanged(nameof(ID));
}
}
private string _longName;
public string LongName
{
get { return _longName; }
set
{
SetValue(ref _longName, value);
OnPropertyChanged(nameof(LongName));
}
}
private string _date;
public string CreateDate
{
get{ return _date;}
set
{
SetValue(ref _date, value);
OnPropertyChanged(nameof(CreateDate));
}
}
private int _prodID;
public int ProductID
{
get { return _id; }
set
{
SetValue(ref _prodID, value);
OnPropertyChanged(nameof(ProductID));
}
}
private string _json;
public string JsonString
{
get { return _json; }
set
{
SetValue(ref _json, value);
OnPropertyChanged(nameof(JsonString));
}
}
private string _location;
public string LocationName
{
get { return _location; }
set
{
SetValue(ref _location, value);
OnPropertyChanged(nameof(LocationName));
}
}
//ADD-ONS
public string Address
{
get
{
if (!string.IsNullOrEmpty(JsonString))
{
var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonString);
if (jsonDict.ContainsKey("address"))
if (jsonDict["address"] != "")
return jsonDict["address"];
}
return null;
}
}
private ImageSource _imageLink;
public ImageSource ImageLink
{
get
{
if(ProductID != 0)
{
...
return ImageSource.FromUri(link);
}
return null;
}
}
#endregion
public SharesViewModel(){}
public SharesViewModel(Share share)
{
ID = share.ID;
ProductID = share.ProductID;
JsonString = share.JsonString;
CreateDate = share.CreateDate;
LocationName = share.LocationName;
}
List View Model -- SharesListViewlModel
public class SharesListViewModel : BaseViewModel
{
private SharesViewModel _selectedShare;
private bool _isDataLoaded;
//grouped list
public ObservableCollection<LocationSpotGroups<string, SharesViewModel>> Shares { get; set; }
...
public ICommand OpenMoreSharesCommand { get; private set; }
public ICommand LoadDataCommand { get; private set; }
public SharesListViewModel(Position NW , Position SE)
{
_nw = NW;
_se = SE;
LoadDataCommand = new Command(async () => await LoadData());
OpenMoreSharesCommand = new Command<SharesViewModel>(async (share) => await OpenMoreShares(share));
public ObservableCollection<SharesViewModel> sList { get; set; }
= new ObservableCollection<SharesViewModel>();
}
private async Task LoadData()
{
if (_isDataLoaded)
return;
var list = await _connection.GetAllRegionShares(_nw, _se);
foreach (var spot in list)
{
sList.Add(new SharesViewModel(spot));
}
var sorted = from item in sList
orderby item.LocationName
group item by item.LocationName into itemGroup
select new LocationSpotGroups<string, SharesViewModel>(itemGroup.Key, itemGroup);
Shares = new ObservableCollection<LocationSpotGroups<string, SharesViewModel>>(sorted);
}
LocationSpotGroups
public class LocationSpotGroups<K, T> : ObservableCollection<T>
{
public K GroupKey { get; set; }
public IEnumerable<T> GroupedItem { get; set; }
public LocationSpotGroups(K key, IEnumerable<T> shares)
{
GroupKey = key;
GroupedItem = shares;
foreach (var item in shares)
{
this.Items.Add(item);
}
}
}
SharesPage XAML
<CollectionView x:Name="CollectionList"
VerticalOptions="FillAndExpand"
ItemsSource="{Binding Shares}"
IsGrouped="True">
<!--HEADER-->
<CollectionView.GroupHeaderTemplate>
<DataTemplate>
<StackLayout Orientation="Horizontal"
Padding="5"
BackgroundColor="#f7f7fb">
<Label x:Name="labelname"
Text="{Binding GroupKey}"
HorizontalOptions="Start"
VerticalOptions="Center"
TextColor="gray" />
</StackLayout>
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="2" />
</CollectionView.ItemsLayout>
<!--BODY-->
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="5" Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ImageButton Source="{Binding ImageLink}"
WidthRequest="150"
HeightRequest="150"
Grid.ColumnSpan="2"
CornerRadius="15"
Aspect="AspectFill"
Grid.Row="0"
Grid.Column="0"/>
<Label Text="{Binding ShoppingName}"
Grid.Row="1"
Grid.Column="0"/>
<Label Text="More"
Grid.Row="1"
Grid.Column="1"
HorizontalTextAlignment="End"/>
<Label Text="{Binding CreateDate}"
Grid.Row="2"
Grid.Column="0"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
SharesPage CS
public SharesPage( Position NW, Position SE )
{
InitializeComponent();
ViewModel = new SharesListViewModel(NW, SE);
}
public SharesListViewModel ViewModel
{
get { return BindingContext as SharesListViewModel; }
set { BindingContext = value; }
}
protected override void OnAppearing()
{
ViewModel.LoadDataCommand.Execute(null);
base.OnAppearing();
}
Loading the data in the constructor works if the data is not a lot, which is wasn't in my case. Everything loads perfectly.
I have implemented a slider menu as ListView and can easily assign the color to any icon in the menu. However, I am trying to change the color of the ListView row that was selected (specifically the icon). I have tried using converter, but the value never changes.
Here is my xaml:
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="SycretBeauty.Screens.MainMenuMaster"
Title="Master"
xmlns:controls="clr-namespace:SycretBeauty;assembly=SycretBeauty"
xmlns:converters="clr-namespace:SycretBeauty.Converters;assembly=SycretBeauty"
BackgroundColor="{StaticResource color_menu_item_background}"
IconImageSource="images/menu.png">
<StackLayout
BackgroundColor="{StaticResource color_header_banner_background}">
<Image
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
Source="images/bannerVialang.png"
Margin="0,30,0,20">
</Image>
<controls:SuperListView
x:Name="MenuItemsListView"
SeparatorVisibility="Default"
HasUnevenRows="False"
Margin="0,0,0,0"
RowHeight="120"
IsScrollingEnable="False"
SelectedItem="{Binding SelectedItem}"
ItemsSource="{Binding MenuItems}"
BackgroundColor="{StaticResource color_menu_item_background}">
<d:ListView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Записаться</x:String>
<x:String>Акции</x:String>
<x:String>Уведомления</x:String>
<x:String>Личный кабинет</x:String>
<x:String>Контакты</x:String>
<x:String>Отзывы</x:String>
<x:String>Галерея</x:String>
<x:String>Поделиться</x:String>
</x:Array>
</d:ListView.ItemsSource>
<controls:SuperListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid
VerticalOptions="FillAndExpand"
ColumnSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackLayout
Grid.Column="0"
BackgroundColor="{StaticResource color_menu_item_background}"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<controls:IconView
VerticalOptions="CenterAndExpand"
HeightRequest="35"
Foreground="{Binding ForegroundColor}"
Source="{Binding ImgSource}">
</controls:IconView>
</StackLayout>
<Label
Grid.Column="0"
Text="{Binding PushCount}"
IsVisible="{Binding PushUnread}"
d:Text="23"
d:IsVisible="True"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
TextColor="White"/>
<StackLayout Grid.Column="1"
BackgroundColor="{StaticResource color_menu_item_background}"
VerticalOptions="CenterAndExpand"
HorizontalOptions="FillAndExpand">
</StackLayout>
<Label Grid.Column="2"
BackgroundColor="{StaticResource color_menu_item_background}"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Text="{Binding Title}"
d:Text="{Binding .}"
Style="{StaticResource font_menu_item_text}"/>
</Grid>
</ViewCell>
</DataTemplate>
</controls:SuperListView.ItemTemplate>
</controls:SuperListView>
</StackLayout>
Here is the model:
public class MainMenuItem
{
public MainMenuItem()
{
TargetType = typeof(MainMenuItem);
}
public int Id { get; set; }
public string ImgSource { get; set; }
public string Title { get; set; }
public Type TargetType { get; set; }
public Action ItemSelected { get; set; }
public string PushCount { get; set; }
public bool PushUnread { get; set; }
public Color ForegroundColor { get; set; }
}
And here is cs:
public partial class MainMenuMaster : ContentPage
{
public ListView ListView;
public AppContext Context { get; set; }
private float rowHeight;
public MainMenuMaster()
{
InitializeComponent();
BindingContext = new ViewModel();
ListView = MenuItemsListView;
ListView.SizeChanged += ListView_SizeChanged;
}
private void ListView_SizeChanged(object sender, EventArgs e)
{
var h = ListView.Height;
rowHeight = (float)(h / 8);
ListView.RowHeight = (int)rowHeight;
}
protected override void OnAppearing()
{
base.OnAppearing();
(BindingContext as ViewModel).UpdatePushCount(Context.UnreadPushCount);
}
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<MainMenuItem> MenuItems { get; set; }
public AppContext Context { get; set; }
public ViewModel()
{
MenuItems = new ObservableCollection<MainMenuItem>(new[]
{
new MainMenuItem {
Id = 0,
ImgSource = "images/signup.png",
Title = "Записаться",
TargetType = typeof(OnlineSignupScreen),
ItemSelected = () => {
Context.SelectSalonStart(typeof(OnlineSignupScreen));
},
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 1,
ImgSource = "images/promos",
Title = "Акции",
TargetType = typeof(PromosScreen),
ForegroundColor = Color.Gray,
},
new MainMenuItem {
Id = 2,
ImgSource = "images/notify",
Title = "Уведомления",
TargetType = typeof(NotificationsScreen),
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 3,
ImgSource = "images/personal",
Title = "Личный кабинет",
TargetType = typeof(PersonalInfoScreen),
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 4,
ImgSource = "images/contacts",
Title = "Контакты",
TargetType = typeof(ContactsScreen),
ItemSelected = () => {
Context.SelectSalonStart(typeof(ContactsScreen));
},
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 5,
ImgSource = "images/feedback.png",
Title = "Отзывы",
TargetType = typeof(FeedbackScreen),
ItemSelected = () => {
Context.SelectSalonStart(typeof(FeedbackScreen));
},
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 6,
ImgSource = "images/gallery.png",
Title = "Галерея",
TargetType = typeof(GalleryScreen),
ItemSelected = () => {
Context.SelectSalonStart(typeof(GalleryScreen));
},
ForegroundColor = Color.Gray
},
new MainMenuItem {
Id = 7,
ImgSource = "images/share",
Title = "Поделиться",
TargetType = typeof(ShareWithFriendScreen),
ForegroundColor = Color.Gray
}
});
}
public void UpdatePushCount(int count)
{
MenuItems[2].ImgSource = count > 0 ? "images/badge" : "images/notify";
MenuItems[2].PushCount = count.ToString();
MenuItems[2].PushUnread = count > 0;
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged == null)
return;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
I also have MainMenu where I control the navigation and have ListView_onSelected method, but even there I can't change the color of the icon.
Please achieve INotifyPropertyChanged interface for MainMenuItem.cs
Then change the ForegroundColor attribute like following code.
public class MainMenuItem: INotifyPropertyChanged
{
public MainMenuItem()
{
TargetType = typeof(MainMenuItem);
}
public int Id { get; set; }
public string ImgSource { get; set; }
public string Title { get; set; }
public Type TargetType { get; set; }
public Action ItemSelected { get; set; }
public string PushCount { get; set; }
public bool PushUnread { get; set; }
private Color _foregroundColor;
public Color ForegroundColor
{
get { return _foregroundColor; }
set
{
_foregroundColor = value;
OnPropertyChanged("ForegroundColor");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I do not what is achievement about controls:SuperListView and controls:IconView, I used listview and ImageButton to replace it, If item was selected. We can change the ImageButton's background color for testing.
<ImageButton
Source="{Binding ImgSource}"
VerticalOptions="CenterAndExpand"
HeightRequest="35"
BackgroundColor="{Binding ForegroundColor}"
>
</ImageButton>
private void MenuItemsListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
// MainMenuItem mainMenuItem = sender as MainMenuItem;
MainMenuItem mainMenuItem = viewModel.MenuItems[e.SelectedItemIndex];
mainMenuItem.ForegroundColor = Color.Red;
}
Here is running GIF.
I have a problem. I created a ListView with a ViewModel. In my ListView I have a few Labels with Text that is bound to the objects in the ItemSource. Now when I change a value in the ViewModel of an item in the ObservableCollection, nothing changes on the screen!
Here is my ViewModel:
public class VM_DeviceList : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ICommand cmdDeleteDevice
{
get
{
return new Command<int>((x) => DeleteDevice_Handler(x));
}
}
public ICommand cmdTogglePower
{
get
{
return new Command<int>((x) => TogglePower_Handler(x));
}
}
private ObservableCollection<DisplayedDevice> _knownDeviceList;
public ObservableCollection<DisplayedDevice> knownDeviceList
{
get
{
return _knownDeviceList;
}
set
{
if (_knownDeviceList != value)
{
_knownDeviceList = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("knownDeviceList"));
}
}
}
}
Here is my class for the ObservableCollection:
public class DisplayedDevice : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public int Id { get; set; }
public string Name { get; set; }
public string State { get; set; }
public string StateShown { get; set; }
public string deviceImage { get; set; }
public string Color { get; set; }
public string PowerStateColor { get; set; }
public string DeviceImageColor { get; set; }
public string DeviceImage
{
get
{
return deviceImage;
}
set
{
deviceImage = value;
OnPropertyChanged();
}
}
}
And here is the xaml:
<ListView ItemsSource="{Binding knownDeviceList}" SelectionMode="None" RowHeight="90" ItemTapped="device_Clicked" x:Name="MyListView">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<AbsoluteLayout HeightRequest="70" Margin="20,10,20,10">
<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>
<Label Grid.Row="0" Grid.Column="1" Text="{Binding Name}" Margin="0,3,0,0"
FontAttributes="Bold" FontSize="24" TextColor="White" />
<Label Grid.Row="1" Grid.Column="1" Text="{Binding StateShown}" FontSize="18" TextColor="White" />
<!-- <Image Source="power" Grid.RowSpan="2" Grid.Column="2" Margin="5" /> -->
<controls:IconView x:Name="btnPower" Source="power" Grid.RowSpan="2" Grid.Column="2" Margin="5"
Foreground="{Binding PowerStateColor}">
<controls:IconView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Path=BindingContext.cmdTogglePower, Source={x:Reference MyListView}}" CommandParameter="{Binding Id}" />
</controls:IconView.GestureRecognizers>
</controls:IconView>
</Grid>
</StackLayout>
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Now what I expected to happen, was that when I click on the IconView I change the IconView color and the label with the StateShown binding. But nothing changes when I click on the IconView!
What am I doing wrong?
Add OnPropertyChanged method call to every property on DisplayedDevice for those you want the UI to notice.
public class DisplayedDevice : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged();
}
}
private int id;
public int Id
{
get
{
return id;
}
set
{
Id = value;
OnPropertyChanged();
}
}
private string state;
public string State
{
get
{
return state;
}
set
{
state = value;
OnPropertyChanged();
}
}
......
}
You can use MVVMHelpers NuGet Package and implement the ObservableObject Class from the NuGet directly to DisplayedDevice class and you need to make all the properties with a reference to the private variable.
public class DisplayedDevice: ObservableObject
{
string _textField = string.Empty;
public string TextField
{
get => _textField;
set => SetProperty(ref _textField, value);
}
bool _isBarChartVisible = false;
public bool IsBarChartVisible
{
get => _isBarChartVisible;
set => SetProperty(ref _isBarChartVisible, value);
}
}
}
Every public property should be backed by a private property of the same type. This is important so that any change in any property will be reflected on the UI using INotifyPropertyChanged
I have a Grouped ListView bound to MyGroup (see below) that contains some property but when that property is being changed the view is not getting updated although I call OnPropertyChanged
My Class
public class MyGroup : ObservableCollection<Items>, INotifyPropertyChanged
{
private string foo;
public string Foo
{
get => foo;
set
{
foo = value;
OnPropertyChanged(nameof(Foo));
}
}
...
}
My View
<ListView ItemSource="{Binding GroupList}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Grid BackgroundColor="#2196F3"
Padding="5,5,5,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label FontSize="Medium" Text="{Binding Header}" Grid.Column="1" FontAttributes="Italic" TextColor="White"/>
<Label Text="{Binding Foo}" TextColor="White" FontSize="Medium" Grid.Column="1" HorizontalOptions="End"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
</ListView>
MyViewModel
public List<MyGroup> GroupList { get => groupList ;set => SetProperty(ref groupList, value); }
According to your description, you bind Foo in ListView Group header, you said that group header don't update when property changed, but I don't see where do you change this property. I've written a sample - I changed the first item group header when button click.
public class PersonList1 : ObservableCollection<Person1>, INotifyPropertyChanged
{
private string _heading;
public string Heading
{
get { return _heading; }
set
{
_heading = value;
RaisePropertyChanged("Heading");
}
}
public ObservableCollection<Person1> Persons => this;
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class Person1
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string DisplayName
{
get
{
return $"{LastName}, {FirstName}";
}
}
}
<StackLayout>
<ListView IsGroupingEnabled="true" ItemsSource="{Binding ListOfPeople}">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Heading}" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding DisplayName}" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button
x:Name="btn1"
Clicked="Btn1_Clicked"
Text="change header" />
</StackLayout>
public partial class Page15 : ContentPage
{
public ObservableCollection<PersonList1> ListOfPeople { get; set; }
public Page15 ()
{
InitializeComponent ();
var sList = new PersonList1()
{
new Person1() { FirstName = "Sally", LastName = "Sampson" },
new Person1() { FirstName = "Taylor", LastName = "Swift" },
new Person1() { FirstName = "John", LastName = "Smith" }
};
sList.Heading = "S";
var dList = new PersonList1()
{
new Person1() { FirstName = "Jane", LastName = "Doe" }
};
dList.Heading = "D";
var jList = new PersonList1()
{
new Person1() { FirstName = "Billy", LastName = "Joel" }
};
jList.Heading = "J";
ListOfPeople = new ObservableCollection<PersonList1>()
{
sList,
dList,
jList
};
this.BindingContext = this;
}
private void Btn1_Clicked(object sender, EventArgs e)
{
ListOfPeople[0].Heading = "this is test";
}
}
You can see the the first ListView group header property change as "this is test".
Your question is not complete enough. Please add detail of how you are calling/setting values to property.
BTW, instead of using this(a wild guess,as you have not provided enough info):
public List<MyGroup> GroupList { get => groupList ;set => SetProperty(ref groupList, value); }
try this:
public List<MyGroup> GroupList {
get{
return groupList;
}
set{
groupList = value;
OnPropertyChanged(nameof(GroupList));
}
I've created a ListView in Xamarin form and bind to Observable collection in view model, adding item dynamically to ListView is working fine by calling OnPropertyChanged event.
But after getting status update from service I'm updating corresponding ListView item status and calling OnPropertyChanged event as well as re-assigining the ListView items to it but didn't get updated GUI properly sometimes working and some times not.
Below is the sample code that I've done.
<ListView Grid.Row="3" HasUnevenRows="True" ItemsSource="{Binding ServiceList}" IsPullToRefreshEnabled="True" SeparatorColor="Black">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical" Spacing="4" Padding="5" BackgroundColor="LightGray">
<Label Text="{Binding OperationStatus, Converter={x:Static local:StatusMessageConverter.Default}}" FontSize="13" FontAttributes="Bold" TextColor="White" BackgroundColor="DarkCyan" />
<Label Text="{Binding Operation}" FontSize="10" Margin="10,0,0,0" />
<Label Text="{Binding OperationType}" FontSize="10" Margin="10,0,0,0" />
<Label Text="{Binding OperationStatus}" LineBreakMode="WordWrap" IsVisible="{Binding CanStatusVisible}" FontSize="10" Margin="10,0,0,0" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
public class ServiceViewModel : INotifyPropertyChanged
{
public ObservableCollection<ServiceItem> ServiceList
{
get
{
return _serviceList;
}
set
{
_serviceList = value;
OnPropertyChanged("ServiceList");
}
}
var tempList = new ObservableCollection<ServiceItem>();
tempList = ServiceList;
var targetItem = from item in tempList
where item.UniqueId == uniqueId
select item;
if (targetItem.Any())
{
var resultItem = targetItem.FirstOrDefault();
resultItem.CanStatusVisible = true;
resultItem.OperationStatus = string.Format("{0}: {1}", "Status Message", resultMessage);
}
ServiceList = null;
ServiceList = tempList;
OnPropertyChanged("ServiceList");
}
public class ServiceItem
{
public string UniqueId { get; set; }
public string Operation { get; set; }
public string OperationType { get; set; }
public string OperationStatus { get; set; }
public string StatusMessage { get; set; }
public bool CanStatusVisible { get; set; }
}
See to it that your model class inherits from INotifyPropertyChangedinterface(as mentioned in the above comments).
public class ServiceItem :INotifyPropertyChanged
{
private string uniqueId,operation,operationType,operationStatus,statusMessage;
private bool statusVisible;
public string UniqueId { get { return uniqueId; } set { uniqueId= value; RaisePropertyChanged(nameof(UniqueId)); } }
public string Operation { get { return operation; } set { operation= value; RaisePropertyChanged(nameof(Operation)); } }
public string OperationType { get { return operationType; } set { operationType= value; RaisePropertyChanged(nameof(OperationType)); } }
public string OperationStatus { get { return operationStatus; } set { operationStatus= value; RaisePropertyChanged(nameof(OperationStatus)); } }
public string StatusMessage { get { return statusMessage; } set { statusMessage= value; RaisePropertyChanged(nameof(StatusMessage)); } }
public bool CanStatusVisible { get { return statusVisible; } set { statusVisible= value; RaisePropertyChanged(nameof(CanStatusVisible )); } }
}
Then your ViewModel code should look something like this:
var tempList = new ObservableCollection<ServiceItem>();
tempList = ServiceList;
var targetItem = from item in tempList
where item.UniqueId == uniqueId
select item;
if (targetItem.Any())
{
var resultItem = targetItem.FirstOrDefault();
resultItem.CanStatusVisible = true;
resultItem.OperationStatus = string.Format("{0}: {1}", "Status Message", resultMessage);
}
ServiceList = null;
ServiceList = tempList;
Once you do these changes your code should work
--- To clarify my comment on FreakyAli's good answer ---
The essential part of FreakyAli's answer is the first code snippet:
public class ServiceItem :INotifyPropertyChanged
...
Once that is done, the other code in question can be greatly simplified. I think (though I have not tested) that you can replace all the code Ali shows under "Then your ViewModel code should look something like this:" with:
ServiceItem resultItem = ServiceList.Where(item => item.UniqueId == uniqueId).FirstOrDefault();
if (resultItem != null)
{
resultItem.CanStatusVisible = true;
resultItem.OperationStatus = string.Format("{0}: {1}", "Status Message", resultMessage);
}
That is, it is not necessary to create a temp list, nor to manipulate ServiceList. When you change the property of a ServiceItem, that property's RaisePropertyChanged will trigger the needed display refresh.