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.
Related
I am trying to show some image only when an item from CollectionView is selected. To do this, I added the "IsSelected" field to the "Category" model and bind to it in CollectionView.ItemTemplate, but after changing the value of "IsSelected" to "true", the image does not appear
model:
public partial class Category : ObservableObject
{
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("mobileImageUrl")]
public string ImageUrl { get; set; }
[JsonPropertyName("isPublic")]
public bool IsPublic { get; set; }
[ObservableProperty]
private bool isSelected;
}
collection view:
<CollectionView
Grid.Row="2" Grid.ColumnSpan="2"
ItemsSource="{Binding Categories}"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedCategories}"
SelectionChangedCommand="{Binding SelectionChangedCommand}">
<CollectionView.Header>
...
</CollectionView.Header>
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="{OnIdiom Default=2}" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Category">
<AbsoluteLayout>
<Image
Aspect="AspectFit"
WidthRequest="165"
Source="pzv.svg"
Margin="0, 10" />
<Image
Aspect="AspectFit"
AbsoluteLayout.LayoutBounds="130, 0, autoSize, autoSize"
Source="selected_category.svg"
IsVisible="{Binding IsSelected, Mode=TwoWay}" >
<Image.Shadow>
<Shadow Brush="Black" Offset="0, 10" Opacity="0.1" Radius="15" />
</Image.Shadow>
</Image>
</AbsoluteLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
viewmodel:
public partial class CatalogPageViewModel : BaseViewModel
{
private readonly CategoryService categoryService;
public ObservableCollection<Category> Categories { get; set; } = new();
[ObservableProperty]
private ObservableCollection<object> selectedCategories = new();
public CatalogPageViewModel(CategoryService categoryService)
{
this.categoryService = categoryService;
InitAsync();
}
public async void InitAsync()
{
var categories = await categoryService.GetCategoriesAsync() ?? new();
if (Categories?.Any() ?? false) Categories.Clear();
foreach (var category in categories) Categories.Add(category);
}
[RelayCommand]
public void OnSelectionChanged()
{
foreach (var selected in SelectedCategories)
{
if (selected is Category category)
{
foreach (var item in Categories.Where(x => x.Id == category.Id))
{
item.IsSelected = true;
}
}
}
...
}
}
I am just new to WPF, Caliburn.Micro and Dapper. I have three combo boxes: the first one is for the region, the second one is for the provinces in the particular selected region and the third one are the cities in the particular selected province. What I want to achieve is that when I selected a particular region it will display all the provinces in that region, the same with the province combo box, when selected it will display all the cities associated with that province. Can this be done in a single method? Here is my code so far.
DataAccess
public List<RegionModel> GetRegion_All()
{
List<RegionModel> output;
using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(GlobalConfig.CnnString(db)))
{
output = connection.Query<RegionModel>("dbo.spRegion_GetAll").ToList();
var p = new DynamicParameters();
foreach (RegionModel region in output)
{
p = new DynamicParameters();
p.Add("#RegionId", region.Id);
region.Provinces = connection.Query<ProvinceModel>("dbo.spProvince_ByRegion", p, commandType: CommandType.StoredProcedure).ToList();
foreach (ProvinceModel province in region.Provinces)
{
p = new DynamicParameters();
p.Add("#ProvinceId", province.Id);
region.Cities = connection.Query<CityModel>("dbo.spCity_ByProvince", p, commandType: CommandType.StoredProcedure).ToList();
}
}
}
return output;
}
Models
public class RegionModel
{
public int Id { get; set; }
public string Region { get; set; }
public string RegionName { get; set; }
public List<ProvinceModel> Provinces { get; set; } = new List<ProvinceModel>();
public List<CityModel> Cities { get; set; } = new List<CityModel>();
public List<BarangayModel> Barangays { get; set; } = new List<BarangayModel>();
}
public class ProvinceModel
{
public int Id { get; set; }
public string Province { get; set; }
public int RegionId { get; set; }
}
public class CityModel
{
public int Id { get; set; }
public string City { get; set; }
public int ProvinceId { get; set; }
public int ZipCode { get; set; }
}
ViewModel
public class ShellViewModel : Screen
{
private BindableCollection<RegionModel> _region;
private RegionModel _selectedRegion;
private ProvinceModel _selectedProvince;
public ShellViewModel()
{
GlobalConfig.InitializeConnections(DatabaseType.Sql);
Region = new BindableCollection<RegionModel>(GlobalConfig.Connection.GetRegion_All());
}
public BindableCollection<RegionModel> Region
{
get { return _region; }
set
{
_region = value;
}
}
public RegionModel SelectedRegion
{
get { return _selectedRegion; }
set
{
_selectedRegion = value;
NotifyOfPropertyChange(() => SelectedRegion);
}
}
public ProvinceModel SelectedProvince
{
get { return _selectedProvince; }
set
{
_selectedProvince = value;
NotifyOfPropertyChange(() => SelectedRegion);
}
}
View
<Window x:Class="WPFUI.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFUI.Views"
mc:Ignorable="d" WindowStartupLocation="CenterScreen"
Title="ShellView" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" x:Name="Region" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Grid.Row="1" x:Name="SelectedRegion_Provinces" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Province}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Grid.Row="2" x:Name="SelectedRegion_Cities" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding City}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Most of my codes ideas are from the tutorials I found in youtube, since references and materials for WPF, Caliburn.Micro and Dapper are very hard to find. Please be patient with my code :)
You have lot of mistake and you dont use the power of Caliburn
The wpf definition:
with Caliburn if you give name Region for combobox, it waits a bindableCollection with same name (Region) and SelectedItem is named SelectedRegion
(see name convention with Caliburn). So i choose Region, Province & City
In the different models i have renamed all strings RegionName, ProvinceName and CityName.
<ComboBox Grid.Row="0" x:Name="Region" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Grid.Row="1" x:Name="Province" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ProvinceName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox Grid.Row="2" x:Name="City" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding CityName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
then, i have modified your class definition:
public class RegionModel:PropertyChangedBase
{
public int Id { get; set; }
public string Region { get; set; }
public string RegionName { get; set; }
// each Region has its Provinces
public List<ProvinceModel> Provinces { get; set; } = new List<ProvinceModel>();
}
public class ProvinceModel:PropertyChangedBase
{
public int Id { get; set; }
public string ProvinceName { get; set; }
public int RegionId { get; set; }
// each Province has its Cities
public List<CityModel> Cities { get; set; } = new List<CityModel>();
}
public class CityModel:PropertyChangedBase
{
public int Id { get; set; }
public string CityName { get; set; }
public int ProvinceId { get; set; }
public int ZipCode { get; set; }
}
Then in ViewModel i add the different BindableCollection Region, Province & City dont forget to add
using Caliburn.Micro;
in the using definion. Then Add the Selected Definition
public class ShellViewModel : Screen
{
private RegionModel selectedRegion;
private ProvinceModel selectedProvince;
private CityModel selectedCity;
private BindableCollection<RegionModel> _region;
private BindableCollection<ProvinceModel> _province;
private BindableCollection<CityModel> _city;
public BindableCollection<RegionModel> Region
{
get { return _region; }
set
{
_region = value;
NotifyOfPropertyChange(() => Region);
}
}
public BindableCollection<ProvinceModel> Province
{
get { return _province; }
set
{
_province = value;
NotifyOfPropertyChange(() => Province);
}
}
public BindableCollection<CityModel> City
{
get { return _city; }
set
{
_city = value;
NotifyOfPropertyChange(() => City);
}
}
public RegionModel SelectedRegion
{
get { return selectedRegion; }
set
{
selectedRegion = value;
NotifyOfPropertyChange(() => SelectedRegion);
Province.Clear();
Province.AddRange(selectedRegion.Provinces);
NotifyOfPropertyChange(() => Province);
}
}
public ProvinceModel SelectedProvince
{
get { return selectedProvince; }
set
{
selectedProvince = value;
NotifyOfPropertyChange(() => SelectedProvince);
City.Clear();
City.AddRange(selectedProvince.Cities);
NotifyOfPropertyChange(() => City);
}
}
public CityModel SelectedCity
{
get { return selectedCity; }
set
{
selectedCity = value;
NotifyOfPropertyChange(() => SelectedCity);
}
}
public ShellViewModel()
{
// to DO INITIALIZE Regions
//
Province = new BindableCollection<ProvinceModel>();
City = new BindableCollection<CityModel>();
Region = new BindableCollection<RegionModel>(Regions);
}
and you have a functional sample
Each time you select a region, the associated provinces are loaded and same thing if you select a province for the associated Cities
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 need to set images to get from a rest service inside every viewer of a ListView. These images change periodically to create a "gif" effect and, after a period, I recall the service to get the updated images about the webcam. The problem is that not all images are set but just a part of them and sometimes no one of them is set.
My code is the following:
public class WebcamListViewModel : BaseViewModel
{
public ICommand InitializeWebcamsCommand { set; get; }
public ICommand OpenVideoWebcamCommand { set; get; }
private List<Webcam> _ListOfWebcam { get; set; }
public List<Webcam> ListOfWebcam
{
get { return _ListOfWebcam; }
set
{
_ListOfWebcam = value;
OnPropertyChanged();
}
}
private IFolder folder;
private int _Counter { get; set; }
public int Counter
{
get { return _Counter; }
set
{
_Counter = value;
OnPropertyChanged();
}
}
private Task SetFrameOnViewTask;
private Task DownloadFramesTask;
CancellationTokenSource tokenSourceSetFrame = new CancellationTokenSource();
CancellationTokenSource tokenSourceDownloadFrames = new CancellationTokenSource();
CancellationToken cancellationTokenSetFrame;
CancellationToken cancellationTokenDownloadFrames;
public WebcamListViewModel(INavigationService navigationService, IApiAutostradeManagerFactory apiAutostradeManagerFactory) : base(navigationService,apiAutostradeManagerFactory)
{
OpenVideoWebcamCommand = new Command<Webcam>(async (webcam) => {
await navigationService.NavigateAsync(Locator.WebcamVideoPopUpPage);
Messenger.Default.Send(new InfoWebcamVideoMessage(webcam.c_mpr, webcam.c_uuid, webcam.t_str_vid));
});
InitializeWebcamsCommand = new Command(async () => await RunSafe(InitializeWebcams()));
InitializeWebcamsCommand.Execute(null);
cancellationTokenDownloadFrames = tokenSourceDownloadFrames.Token;
DownloadFramesTask = new Task(async () => {
cancellationTokenDownloadFrames.ThrowIfCancellationRequested();
while (true)
{
try
{
await DownloadAndSetWebcamImages();
await Task.Delay(2000);
if (cancellationTokenDownloadFrames.IsCancellationRequested)
{
// Clean up here, then...
cancellationTokenDownloadFrames.ThrowIfCancellationRequested();
}
}
catch (System.FormatException e)
{
Console.WriteLine(e.Message);
}
}
}, cancellationTokenDownloadFrames);
SetFrameOnViewTask = new Task(async () =>
{
cancellationTokenSetFrame.ThrowIfCancellationRequested();
while (true)
{
try
{
Counter++;
await Task.Delay(500);
if (cancellationTokenSetFrame.IsCancellationRequested)
{
Counter = 0;
// Clean up here, then...
cancellationTokenSetFrame.ThrowIfCancellationRequested();
}
}
catch (FormatException e)
{
Console.WriteLine(e.Message);
}
}
}, cancellationTokenSetFrame);
}
private async Task InitializeWebcams()
{
folder = await FileSystem.Current.LocalStorage.GetFolderAsync("WebcamImages");
ListOfWebcam = await RepositoryHelper.Instance.WebcamRepository.GetItemsAsync();
ListOfWebcam = ListOfWebcam.OrderByDescending(x => x.n_prg_km).ToList();
try
{
if (DownloadFramesTask.Status == TaskStatus.Running)
{
try
{
tokenSourceDownloadFrames.Cancel();
}
finally
{
tokenSourceDownloadFrames.Dispose();
}
}
DownloadFramesTask.Start();
if (SetFrameOnViewTask.Status == TaskStatus.Running)
{
try
{
tokenSourceSetFrame.Cancel();
}
finally
{
tokenSourceSetFrame.Dispose();
}
}
SetFrameOnViewTask.Start();
}
catch (System.InvalidOperationException)
{}
}
private async Task DownloadAndSetWebcamImages()
{
await ImageService.Instance.InvalidateCacheAsync(CacheType.All);
foreach (var web in ListOfWebcam)
{
web.image1 = await GetWebcamFrame(web.frame1);
web.image2 = await GetWebcamFrame(web.frame2);
web.image3 = await GetWebcamFrame(web.frame3);
web.image4 = await GetWebcamFrame(web.frame4);
}
}
private async Task<ImageSource> GetWebcamFrame(string urlFrame)
{
try
{
var frameResponse = await ApiManager.GetWebcamFrame(urlFrame);
var base64Image = await frameResponse.Content.ReadAsStringAsync();
byte[] imageData = Convert.FromBase64String(base64Image);
return (ImageSource.FromStream(() => { return new MemoryStream(imageData); }));
}
catch (FormatException e)
{
throw e;
}
}
In my viewModel, I got two tasks: DownloadFramesTask and SetFrameOnViewTask, that every 500 ms increment a counter, that is used to show one of the four frames at the turn.
<ListView ItemsSource="{Binding ListOfWebcam}"
SeparatorVisibility="None"
CachingStrategy="RetainElement"
RowHeight="250"
VerticalOptions="FillAndExpand"
x:Name="ListWebcam">
<ListView.Header>
<StackLayout x:Name="HeaderStackLayout"
Padding="5,25,0,30"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand">
<Label x:Name="LabelHeader"
Text="Webcam:"
FontSize="Large"
FontAttributes="Bold"
TextColor="{x:Static statics:Palette.PrimaryColor}"
VerticalOptions="Center"
HorizontalOptions="Start" Margin="10,0,0,0"/>
</StackLayout>
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<controls:ExtendedViewCell SelectedItemBackgroundColor="#fafafa">
<Grid x:Name="GridWebcam">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Frame Grid.Column="1"
Grid.RowSpan="2"
CornerRadius="20"
BackgroundColor="{x:Static statics:Palette.PrimaryColor}"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"
HasShadow="True"
Margin="5,10">
<StackLayout>
<Label Text="{Binding t_str_vid,Converter={StaticResource WebcamNameConverter}}"
FontSize="Medium"
TextColor="White"
FontAttributes="Bold"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
</Label>
<Label TextColor="White"
FontSize="Medium"
Text="{Binding direzione,Converter={StaticResource DirectionToStringConverter}}"/>
<StackLayout Orientation="Horizontal">
<ffimageloading:CachedImage DownsampleToViewSize="True"
VerticalOptions="FillAndExpand"
HorizontalOptions="StartAndExpand"
IsVisible="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame1Converter}}"
IsEnabled="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame1Converter}}"
Source="{Binding image1}"/>
<ffimageloading:CachedImage x:Name="SecondFrame"
DownsampleToViewSize="True"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame2Converter}}"
IsEnabled="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame2Converter}}"
VerticalOptions="FillAndExpand"
HorizontalOptions="StartAndExpand"
Source="{Binding image2}"/>
<ffimageloading:CachedImage x:Name="ThirdFrame"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame3Converter}}"
IsEnabled="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame3Converter}}"
VerticalOptions="FillAndExpand"
HorizontalOptions="StartAndExpand"
Source="{Binding image3}"/>
<ffimageloading:CachedImage x:Name="FourthFrame"
Grid.Row="1"
Grid.Column="0"
IsVisible="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame4Converter}}"
IsEnabled="{Binding Source={x:Reference WebcamList},Path=BindingContext.Counter,Converter={StaticResource VisibleFrame4Converter}}"
VerticalOptions="FillAndExpand"
HorizontalOptions="StartAndExpand"
Source="{Binding image4}"/>
<iconize:IconButton Text="fas-play-circle"
FontSize="50"
HorizontalOptions="EndAndExpand"
VerticalOptions="EndAndExpand"
TextColor="White"
Command="{Binding BindingContext.OpenVideoWebcamCommand, Source={x:Reference ListWebcam}}"
CommandParameter="{Binding}"
BackgroundColor="Transparent"/>
</StackLayout>
</StackLayout>
</Frame>
</Grid>
</controls:ExtendedViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In My dataTemplate I bind isVisible and isEnabled of every image with the counter, that is converted into a boolean thanks to the four converters.
I'll show just one of them:
public class VisibleFrame1Converter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((int)value % 4 == 0)
return true;
else
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This one, for example, is used to show the first frame when the condition about counter is satisfied.
My model class is the following:
public class Webcam : INotifyPropertyChanged
{
[PrimaryKey, AutoIncrement]
public int idWebcam { get; set; }
public string c_mpr { get; set; }
public int c_tel { get; set; }
public string c_uuid { get; set; }
public string direzione { get; set; }
public string frame1 { get; set; }
public string frame2 { get; set; }
public string frame3 { get; set; }
public string frame4 { get; set; }
public double n_crd_lat { get; set; }
public double n_crd_lon { get; set; }
public int n_ind_pri { get; set; }
public double n_prg_km { get; set; }
public int ramo { get; set; }
public int str { get; set; }
public string strada { get; set; }
public string t_str_vid { get; set; }
public string thumb { get; set; }
public ImageSource image1 { get; set; }
public ImageSource image2 { get; set; }
public ImageSource image3 { get; set; }
public ImageSource image4 { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The expected result is to show every frame downloaded (I'm sure that every frame is downloaded, I checked one by one) in the view and download the updated version every tot time.
Maybe your problem is Webcam.cs , also need to use INotifyPropertyChanged for its property. As follow:
public class Webcam : INotifyPropertyChanged
{
[PrimaryKey, AutoIncrement]
public int idWebcam { get; set; }
public string c_mpr { get; set; }
public int c_tel { get; set; }
public string c_uuid { get; set; }
public string direzione { get; set; }
public string frame1 { get; set; }
public string frame2 { get; set; }
public string frame3 { get; set; }
public string frame4 { get; set; }
public double n_crd_lat { get; set; }
public double n_crd_lon { get; set; }
public int n_ind_pri { get; set; }
public double n_prg_km { get; set; }
public int ramo { get; set; }
public int str { get; set; }
public string strada { get; set; }
public string t_str_vid { get; set; }
public string thumb { get; set; }
// modified code
ImageSource image1 ;
public ImageSource Image1
{
set
{
if (image1 != value)
{
image1 = value;
OnPropertyChanged("Image1");
}
}
get
{
return image1 ;
}
}
ImageSource image2 ;
public ImageSource Image2
{
set
{
if (image2 != value)
{
image2 = value;
OnPropertyChanged("Image2");
}
}
get
{
return image2 ;
}
}
ImageSource image3 ;
public ImageSource Image3
{
set
{
if (image3 != value)
{
image3 = value;
OnPropertyChanged("Image3");
}
}
get
{
return image3 ;
}
}
ImageSource image4 ;
public ImageSource Image4
{
set
{
if (image4 != value)
{
image4 = value;
OnPropertyChanged("Image4");
}
}
get
{
return image4 ;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Any other you want updated property ,all need to use OnPropertyChanged in Model.Just in WebcamListViewModel.cs using , Webcam's property can not work.
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.