Displaying duplicate when adding a new item to an ObservableCollection<> bound to a Collection View in .net Maui - xaml

I currently have a Collection View that displays a list of records being pulled from a local database in my device. The database is working fine, adding the records, deleting them is working fine. The problem is that when I add or delete a record, when the collection view gets refreshed it is displaying a duplicate of each existing record. The weird part is that if I refresh again, it goes back to normal and only shows the records of the table in the database that is pulling from.
Here is my view Model:
[QueryProperty(nameof(Players), "Players")]
public partial class ManagePlayersPageViewModel : ObservableObject
{
/// <summary>
/// List of players being displayed
/// </summary>
private ObservableCollection<Player> _players = new();
public ObservableCollection<Player> Players
{
get => _players;
set => SetProperty(ref _players, value);
}
[ObservableProperty] private bool isRefreshing;
/// <summary>
/// Options for selection modes
/// </summary>
public SelectionOptions SelectionOptions { get; } = new();
/// <summary>
/// Adds player to list
/// </summary>
/// <returns></returns>
[RelayCommand]
async Task AddPlayer()
{
var task = await Shell.Current.ShowPopupAsync(new AddPlayerPopup());
var player = task as Player;
if (task == null)
return;
if (await PlayerService.RecordExists(player))
{
await Shell.Current.DisplaySnackbar("Jugador ya existe");
return;
}
await PlayerService.AddAsync(player);
await Refresh();
}
Here is the refresh() method:
/// <summary>
/// Refreshs and updates UI after each database query
/// </summary>
/// <returns></returns>
[RelayCommand]
async Task Refresh()
{
IsRefreshing = true;
await Task.Delay(TimeSpan.FromSeconds(1));
Players.Clear();
var playersList = await PlayerService.GetPlayersAsync();
foreach (Player player in playersList)
Players.Add(player);
IsRefreshing = false;
}
Here is my xaml where the control sits:
<RefreshView Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
IsRefreshing="{Binding IsRefreshing}"
Command="{Binding RefreshCommand}">
<CollectionView
ItemsSource="{Binding Players}"
SelectionMode="{Binding SelectionOptions.SelectionMode}">
<CollectionView.ItemTemplate>
<DataTemplate>
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItemView
Padding="0, 2.5"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ManagePlayersPageViewModel}}, Path= DeletePlayerCommand}"
CommandParameter="{Binding .}">
<Border
StrokeShape="RoundRectangle 10"
Stroke="{StaticResource DimBlackSolidColorBrush}"
Background="{StaticResource DimBlackSolidColorBrush}">
<Grid>
<Image
Source="Resources/Images/delete.svg"
WidthRequest="35"
HeightRequest="35"
Aspect="AspectFill"/>
</Grid>
</Border>
</SwipeItemView>
</SwipeItems>
</SwipeView.RightItems>
<Grid>
<Border Grid.Column="0"
StrokeShape="RoundRectangle 10"
Stroke="{StaticResource DimBlackSolidColorBrush}"
StrokeThickness="3">
<Grid
RowDefinitions="auto, auto, auto"
Background="{StaticResource DimBlackSolidColorBrush}">
<Label Grid.Row="0"
Text="{Binding Name}"
VerticalTextAlignment="Center"
Margin="10, 2.5"
TextColor="White"/>
<Label Grid.Row="1"
Text="{Binding Alias}"
VerticalTextAlignment="Center"
Margin="10, 2.5" />
<Label Grid.Row="2"
Text="{Binding Team, TargetNullValue=Ninguno}"
VerticalTextAlignment="Center"
FontAttributes="Italic"
Margin="10, 2.5" />
</Grid>
</Border>
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:ManagePlayersPageViewModel}}, Path=ItemTappedCommand}"
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
Any idea why this might be happening?
Note: The database is being queried in a previous page, and is being passed as an argument to the page where the collection view sits, do not know if that has anything to do with it.
Note: It used to work fine with the List View control, but I dont have as much flexibility for customization with that control which is why im going the route of using a Collection View.
When I debug, it is showing me that the value in the setter is already the duplicate but I have no clue on why or where is getting duplicated. It only happens when I add or delete a record.
Any help is appreciated thanks!

The symptom could happen if Refresh is called again, before it finishes. Specifically, if it is called again during await PlayerService.GetPlayersAsync(), the sequence of actions on Players would be:
call#1: Players.Clear();
call#2: Players.Clear();
call#1: add all (existing) players
call#2: add all players (including new one).
To find out if this is happening, add a check:
async Task Refresh()
{
if (IsRefreshing)
throw new InvalidProgramException("Nested call to Refresh");
IsRefreshing = true;
try
{
... your existing code ...
}
finally
{
IsRefreshing = false;
}
}
Does this ever throw that exception?
If so, then move Players.Clear() later in code. This might fix it:
var playersList = await PlayerService.GetPlayersAsync();
Players.Clear();
If that doesn't fix it, then add a lock around the statements that touch Players, to ensure that one call can't touch it until previous one is done:
...
lock (PlayersLock)
{
Players.Clear();
foreach (Player player in playersList)
Players.Add(player);
}
...
// OUTSIDE of the method, define a member to act as a lock:
private object PlayersLock = new object();

Related

How to fire a command after clicking somewhere else?

So, I have a custom control, CusConA, that works basically like a textbox - you type amount of money that you need, and I have a button below, whom by getting clicked saves that amount(from CusConA) somewhere, and that is working fine.
But I want to try the same functionality basically by clicking anywhere on that page (something like OnBlur in asp.net), or to be precise, when my CusConA is not in focus anymore.
By doing what is shown with the --> in code, I achieved sort of a solution, this way when pressing anywhere, even if I never even tried to write an amount, the command is being executed.
So, to try to circle my question, I need this command to execute only after typing some amount, and clicking somewhere alse after. How can I do that?
<Frame
Margin="55,0"
Padding="0"
BorderColor="Blue"
CornerRadius="30">
<StackLayout Orientation="Horizontal">
<Label
Margin="10"
FontAttributes="Bold"
FontSize="20"
HorizontalTextAlignment="Center"
Text="RSD"
TextColor="Some text"
VerticalTextAlignment="Center" />
<customControls:CusConA
Margin="0,0,15,0"
HorizontalOptions="FillAndExpand"
Keyboard="Numeric"
Placeholder="0,00"
PlaceholderColor="Gray"
Text="Some text"
TextColor="Black" >
--> <customControls:CusConA.Behaviors>
<xct:EventToCommandBehavior EventName="Unfocused" Command="{Binding DoSomething}" ></xct:EventToCommandBehavior>
</customControls:CusConA.Behaviors>
</customControls:CusConA>
</StackLayout>
</Frame>
Can you change DoSomething to check whether the amount has been typed? Might involve adding a boolean property to your control:
bool CanExecute { get; set; }
Then have "amount" bound to a property whose setter sets CanExecute = true; or CanExecute = false;, depending on whether an amount has been typed. Something like:
string Amount
{
...
set {
_amount = value;
myControl.CanExecute = value.Count > 0;
}
}
Then change DoSomething body to
if (this.CanExecute) { ... }
Alternatively, other techniques can be used to have a change to Amount trigger a change to a property on myControl.
The essential points are:
Adding CanExecute property, so control can be told when it is valid to execute that command.
Using some technique to bind or trigger myControl.CanExecute change, from elsewhere.
I think you can use EventToCommandBehavior to achieve this function.
There is an example of an EventToCommandBehavior in the Xamarin.Forms samples (see here).
<ContentPage.BindingContext>
<focusapp:MyViewModel></focusapp:MyViewModel>
</ContentPage.BindingContext>
<StackLayout>
<Entry>
<Entry.Behaviors>
<Behaviors:EventToCommandBehavior
EventName="Unfocused"
Command="{Binding EntryUnfocused}" />
</Entry.Behaviors>
</Entry>
</StackLayout>
And define EntryUnfocused in your viewmodel.cs (e.g. MyViewModel) just as follows:
MyViewModel.cs
public class MyViewModel
{
public ICommand EntryUnfocused { get; protected set; }
public MyViewModel() {
EntryUnfocused = new Command(CompletedCommandExecutedAsync);
}
private void CompletedCommandExecutedAsync(object param)
{
System.Diagnostics.Debug.WriteLine("------------> come here....");
}
}

How to use x:Bind with different data type than data template

I'm working on a view (called 'Familify') which shows users a list of assets, and allows them to delete an asset from the list. The assets are stored in an ObservableCollection in the ViewModel, so the command to delete simply takes the asset object and removes it from collection. I'm having issues getting the 'delete' functionality working. Here is the XAML and codebehind:
Familify.xaml
<ListView
ItemsSource="{Binding Assets}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150px" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60px" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="{Binding number}" FontFamily="Consolas"/>
<TextBlock
Grid.Column="1"
Text="{Binding type}"/>
<TextBlock
Grid.Column="2"
Text="add binding here"/>
<TextBlock
Grid.Column="3"
Text="add binding here"/>
<Button
Command="{x:Bind ViewModel.RemoveAssetCommand}"
CommandParameter="{Binding}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Familify.xaml.cs
namespace asset_manager.Views
{
public sealed partial class Familify : UserControl
{
FamilifyViewModel ViewModel { get; set; }
public Familify()
{
this.InitializeComponent();
DataContextChanged += (s, e) =>
{
ViewModel = DataContext as FamilifyViewModel;
};
}
}
}
The idea is that clicking the button removes the asset from the list. (Just to note, the normal binding showing number, type, etc. is working correctly.) My thinking so far:
Try to use binding to access the RemoveAssetCommand stored in the View Model for the page. However, I couldn't get ancestral binding to work (i.e. trying to find the data context of an element higher up in the XAML hierarchy didn't work because findAncestor isn't a thing in UWP.)
x:Bind looked like a good solution, because it uses an explicit path to the property. So, if I declared ViewModel in my code behind, I could use x:Bind ViewModel.property. All well and good. I did just that, and intellisense allowed me to access the ViewModel.RemoveAssetCommand when typing it out.
However, this did not work, because I get the error no DataType defined for DataTemplate. This makes sense, so I tried two things.
x:DataType="Models:Asset" (put in the DataTemplate tag above) is the model being shown in the data template, so I tried that first. Of course, the command is not declared in the model, it's declared in the View Model, so that didn't work.
I instead tried x:DataType="ViewModels:FamilifyViewModel", thinking I could just use x:Bind with that. However, I then got an error that it couldn't cast an object of type Asset to FamilifyViewModel. This makes sense, because the object getting passed to this data template is of the type Asset.
This is a pain, because the whole reason I thought x:Bind would work is that I could just access the property directly from the ViewModel in the codebehind.
Explicitly stated, 1) is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel? and 2) is there a better way to go about implementing this functionality?
Is it possible to use x:Bind within a data template to access a base level property (in this case, a Prism command) on the ViewModel?
Yes, if you want to access a base level, you can reassign DataContext of button like following:
<Button DataContext="{Binding ElementName=Familily, Path=DataContext}"/>
The Family is the name of UserControl.
is there a better way to go about implementing this functionality?
When you put commad in the ViewModel and bind the button as above. The the bind item of button will become Family DataContext. So you could not invoke delete action directly in the ViewModel.
The best practice to implement this functionality is that put the RemoveAssetCommand in the Asset class. And use the ItemsSource of ListView as Button CommandParameter.
<Button
Command="{Binding RemoveAssetCommand}"
CommandParameter="{Binding ElementName=MyListView, Path=ItemsSource}"
Content=""
FontFamily="Segoe MDL2 Assets"
Grid.Column="4">
</Button>
Asset.cs
public class Asset
{
public string number { get; set; }
public string type { get; set; }
public ICommand RemoveAssetCommand
{
get
{
return new CommandHandler<ObservableCollection<Asset>>((item) => this.RemoveAction(item));
}
}
private void RemoveAction(ObservableCollection<Asset> items)
{
items.Remove(this);
}
}
ViewModel.cs
public class FamilifyViewModel
{
public ObservableCollection<Asset> Assets = new ObservableCollection<Asset>();
public FamilifyViewModel()
{
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
Assets.Add(new Asset { number = "100001", type = "hello" });
}
}

Interaction Trigger before selectionChanged of ListPicker in Windows Phone 8

I have a issue when trigger comes in ViewModel the SelectedItem(parameter) comes the previously selected Item. I need the newly selected item as parameter on selectionChanged.
I am new in WP8. Below is the code
<toolkit:ListPicker Header="Background"
ExpansionMode="FullscreenOnly"
Template="{StaticResource ListPickerControlTemplate}"
VerticalAlignment="Top"
ItemsSource="{Binding Path=Buildings.ObjectList}"
Margin="0"
x:Name="buldings"
Padding="0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Path=BuildingSelectionCommand}"
CommandParameter="{Binding Path=SelectedItem, ElementName=buldings}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Thanks
Vinod
Normally should you get the currently SelectionItem passed as parameter to your Command. Since the event is written in past tense, so that you should get the currently SelectedItem and not the previously one.
What you can try is to add a binding for the SelectedItem to your ListPicker and omit passing the SelectedItem to your Command as parameter.
<toolkit:ListPicker SelectedItem="{Binding SelectedBuilding, Mode=TwoWay}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Path=BuildingSelectionCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</toolkit:ListPicker>
Your command then needs to access the property SelectedBuilding to execute
public class BuildingSelectionCommand{
// a reference to your ViewModel that contains the SelectedBuilding-Property
public BuildingsViewModel ViewModel {
get;
set;
}
public bool CanExecute(object parameter) {
return ViewModel.SelectedBuilding != null;
}
public void Execute(object parameter){
var selectedItem = ViewModel.SelectedBuilding;
// execute command logic with selectedItem
}
}
The code can be diffrent on your side, cause it depends on how you have implemented your ViewModel and Command, but i think you should get it.
Another way without using an EventTrigger, is to execute the command directly in your SelectedBuilding-Property.
public Building SelectBuilding{
get {
return _selectedBuilding
}
set{
_selectedBuilding = value;
RaisePropertyChanged("SelectedBuilding");
if (BuildingSelectionCommand.CanExecute(_selectedBuilding)) {
BuildingSelectionCommand.Execute(_selectedBuilding);
}
}
XAML:
<i:EventTrigger EventName="SelectionChanged">
<command:EventToCommand Command="{Binding BuildingSelectionCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
Command:
RelayCommand<SelectionChangedEventArgs> BuildingSelectionCommand { get; set; }
BuildingSelectionCommand = new RelayCommand<SelectionChangedEventArgs>(async (args) => { });
You just missed the "PassEventArgsToCommand". Make sure your command has a SelectionChangedEventArgs.
The simple way to solve it is to use
SelectedItem="{Binding SelectedBuilding, Mode=TwoWay}"
as Jehof suggested and get rid of all the "<i:" trigger settings but simply handle the change in the SelectedBuilding property setter and call a method instead of using commands to wrap a method call. You are not gaining anything with a command since you are not even using CanExecute here, but simply adding more code.

custom live tile update issue

I'm working on a weather application for windows phone. One of the features that I want to take advantage of is live tiles. I have a background agent that runs when the user pins a city to the start page.
After it's been pinned, it makes a calls out to the internet to get some weather data. All of this works just fine.
Now for the problem.
Depending on the weather data that's returned, I want to update the tiles that are pinned to the start screen.
I have a number of different .xaml files (rain, snow, sun, etc) that represent each tile.
My first thought was that I would:
expose 2 properties on each tile (CityState and Temp)
set those 2 properties after the tile is created.
save the tile off into IsolatedStorage as an image that I can then use to update the tile on the start screen.
Here is the code that I have to do that:
var ctl = new Snow();
//just some dummy data to test
ctl.CityState = "Test, NY";
ctl.Temp = 25;
ctl.Measure(new Size(173, 173));
ctl.Arrange(new Rect(0, 0, 173, 173));
var bmp = new WriteableBitmap(173, 173);
bmp.Render(ctl, null);
bmp.Invalidate();
var iss =IsolatedStorageFile.GetUserStoreForApplication();
var filename = "/Shared/ShellContent/tileTest.jpg";
using (var stm = iss.CreateFile(filename))
{
bmp.SaveJpeg(stm, 173, 173, 0, 80);
}
tile.BackgroundImage = new Uri("isostore:" + filename, UriKind.Absolute);
var tileToUpdate = ShellTile.ActiveTiles.FirstOrDefault(r => r.NavigationUri == uri);
tileToUpdate.Update(tile);
So, when this runs, it creates a new tile from the XAML file and updates the start screen but the Temp and CityState properties
are not reflected on the new Tile. In the xaml I have 2 textblocks that are bound to the properties in the codebehind. I've also
implemented INotifyPropertyChanged.
Here is the XAML
<UserControl
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"
mc:Ignorable="d"
x:Name="Window"
x:Class="ezweather.services.tiles.Snow"
d:DesignWidth="480" d:DesignHeight="800" Width="173" Height="173" >
<Canvas x:Name="Layer_1" Width="173" Height="173" Canvas.Left="0" Canvas.Top="0" >
<Rectangle x:Name="Rectangle" Width="173" Height="173" Canvas.Left="0" Canvas.Top="-1.52588e-005" Stretch="Fill" Fill="#FF3F6A8D"/>
<TextBlock x:Name="cityState" TextAlignment="Left" FontFamily="Segoe UI Semibold" FontWeight="Bold" FontSize="15" Width="Auto" Height="Auto" Canvas.Left="0" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<MatrixTransform Matrix="1.33333,0,0,1.33333,11,139.5"/>
</TransformGroup>
</TextBlock.RenderTransform>
<Run Text="{Binding ElementName=Window, Path=CityState}" Foreground="#FFFFFFFF"/>
</TextBlock>
<TextBlock x:Name="temp" TextAlignment="Right" FontFamily="Segoe UI Light" FontSize="44" Width="Auto" Height="Auto" Canvas.Left="0" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<MatrixTransform Matrix="1.33333,0,0,1.33333,87.57,42.9333"/>
</TransformGroup>
</TextBlock.RenderTransform>
<Run Text="{Binding ElementName=Window, Path=Temp}" Foreground="#FFFFFFFF"/>
</TextBlock>
</Canvas>
</UserControl>
and here is the codebehind
public partial class Snow : UserControl, INotifyPropertyChanged
{
public Snow()
{
// Required to initialize variables
InitializeComponent();
}
private string _cityState;
private int _temp;
public string CityState
{
get { return _cityState; }
set
{
_cityState = value;
RaisePropertyChanged("CityState");
}
}
public int Temp
{
get { return _temp; }
set
{
_temp = value;
RaisePropertyChanged("Temp");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string property)
{
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
When this code runs, it instantiates the correct xaml file and saves it to disk.
It then updates the tile on the start screen but the CityState and Temp data does not show up.
I don't know why the CityState and Temp data isn't being written out with the image.
What am I missing?
The primary issue I see here, is you're attempting to render the image, before the control is actually loaded.
Try handle the rendering in the Control.Loaded event.

How to attach EventTrigger in code behind in Silverlight 4

My question is the following:
I have a grid and I attached the SelectedIndexChanged event the following way in the xaml file:
"<cc:DetailViewGrid AutoGenerateColumns="False" HorizontalAlignment="Stretch" Margin="0,0,0,0" Name="dgAcitivityList" VerticalAlignment="Stretch" ItemsSource="{Binding EntityList}" SelectionMode="Single" IsReadOnly="False">
<interactivity:Interaction.Triggers>
<interactivity:EventTrigger EventName="SelectionChanged">
<interactivity:InvokeCommandAction Command="{Binding SelectedItemChangeCommand}" CommandParameter="{Binding SelectedItem, ElementName=dgAcitivityList}"/>
</interactivity:EventTrigger>
</interactivity:Interaction.Triggers>"
But I want to attach this event in code behind. I ctreated an own grid that is inherited from windows grid, and I put this code to own control.
public override void OnApplyTemplate()
{
//base.OnApplyTemplate();
System.Windows.Interactivity.EventTrigger selectedItemChangedTrigger = new System.Windows.Interactivity.EventTrigger("SelectionChanged");
System.Windows.Interactivity.InvokeCommandAction action = new System.Windows.Interactivity.InvokeCommandAction();
action.CommandName = "{Binding SelectedItemChangeCommand}";
action.CommandParameter = string.Format("{{Binding SelectedItem, ElementName={0}}}", this.Name);
selectedItemChangedTrigger.Actions.Add(action);
System.Windows.Interactivity.Interaction.GetTriggers(this).Add(selectedItemChangedTrigger);
base.OnApplyTemplate();
}
Is this solution proper? It's not working but I'm not sure that I should put this code in the OnApplyTemplate() method.