I am working on an App, in which I am able to attach attachments like images and audio to a profile and display those attachments in CreateAndUpdateRiskProfileView. I can add attachments with the AddAttachmentModal. When I choose an attachment there it will be automatically displayed in the CreateAndUpdatetRiskProfileView. Now I also want to be able to delete attachments permanently from the database. This is possible when I longpress on an attachment in the AddAttachmentModal and select delete. Now this works and the attachment is not being displayed in the AddAttachmentModal and if I go to another profile which used this attachment, the attachment is also gone. However, in the CreateAndUpdateRiskProfileView of the Profile where I just deleted an attachment this attachment is still there and will remain there until I close the App and build again.
I want the deleted attachment to not show anymore imidiatly, but I don't know how I can make this happen.
I would appreciate the help!
This is the CreateAndUpdateRiskProfileView :
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:SiRiAs.Controls"
xmlns:flowlayout="clr-namespace:SiRiAs.Controls.FlowLayout"
xmlns:behaviours="clr-namespace:SiRiAs.Behaviors"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:convertes="clr-namespace:SiRiAs.Lib.Converters;assembly=SiRiAs.Lib"
xmlns:selectors="clr-namespace:SiRiAs.Lib.TemplateSelectors;assembly=SiRiAs.Lib"
xmlns:viewmodel="clr-namespace:SiRiAs.Lib.ViewModels;assembly=SiRiAs.Lib"
xmlns:models="clr-namespace:SiRiAs.Lib.Models;assembly=SiRiAs.Lib"
x:Class="SiRiAs.Views.CreateAndUpdateRiskProfileView"
x:DataType="viewmodel:CreateAndUpdateRiskProfileViewModel"
Shell.NavBarIsVisible="False">
<ContentPage.Resources>
<ResourceDictionary>
<convertes:LocationToStringConverter x:Key="LocationToStringConverter"
DefaultConvertReturnValue="0°0'0''N 0°0'0''W"/>
<DataTemplate x:Key="ImageAttachmentView"
x:DataType="models:Attachment">
<controls:LongPressImageButton Style="{StaticResource PreviewImageButtonStyle}"
Aspect="AspectFill"
HeightRequest="70"
WidthRequest="70"
Source="{Binding Link}"
LongPressCommand="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:CreateAndUpdateRiskProfileViewModel}}, Path=ShowDeleteOptionCommand}"
LongPressCommandParameter="{Binding .}"
/>
</DataTemplate>
<DataTemplate x:Key="AudioAttachmentView"
x:DataType="models:Attachment">
<controls:LongPressButton Style="{StaticResource PreviewButtonStyle}"
Text="{Binding Name}"
HeightRequest="70"
WidthRequest="70"
LongPressCommand="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:CreateAndUpdateRiskProfileViewModel}}, Path=ShowDeleteOptionCommand}"
LongPressCommandParameter="{Binding .}"/>
</DataTemplate>
This is the CreateAndUpdateProfileView.xaml.cs
using SiRiAs.Lib.ViewModels;
namespace SiRiAs.Views;
public partial class CreateAndUpdateRiskProfileView : ContentPage {
private readonly CreateAndUpdateRiskProfileViewModel _viewModel;
public CreateAndUpdateRiskProfileView(CreateAndUpdateRiskProfileViewModel createAndUpdateRiskProfileViewModel) {
InitializeComponent();
_viewModel = createAndUpdateRiskProfileViewModel;
BindingContext = createAndUpdateRiskProfileViewModel;
}
protected override bool OnBackButtonPressed() {
if(_viewModel.ReturnOrPopupCommand.CanExecute(null)) {
_viewModel.ReturnOrPopupCommand.Execute(null);
}
return true;
}
}
This is the CreateAndUpdateRiskProfileViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SiRiAs.Lib.Helpers;
using SiRiAs.Lib.Models;
using SiRiAs.Lib.Repositories;
using SiRiAs.Lib.Services;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace SiRiAs.Lib.ViewModels {
[QueryProperty(nameof(Intent), nameof(Intent))]
[QueryProperty(nameof(Model), nameof(Model))]
[QueryProperty(nameof(ActivityId), nameof(ActivityId))]
public partial class CreateAndUpdateRiskProfileViewModel : ObservableObject {
private readonly IUnitOfWork _unitOfWork;
private readonly IPopupProvider _popupProvider;
private bool isModelDirty;
public Guid ActivityId { get; set; }
[ObservableProperty]
private Intent intent = Intent.Create;
[ObservableProperty]
private RiskProfile model = new();
[ObservableProperty]
private ObservableCollection<Attachment> attachments = new();
//...
public CreateAndUpdateRiskProfileViewModel(IUnitOfWork unitOfWork, IPopupProvider popupProvider) {
_unitOfWork = unitOfWork;
_popupProvider = popupProvider;
PopulateRiskCategories();
}
/// <summary>
/// Populate fields if model is loaded from database
/// </summary>
/// <param name="value">model</param>
partial void OnModelChanged(RiskProfile value) {
if (value is not null) {
CreationTimeStamp = Model.CreationDate;
SelectedRiskCategory = AvailableRiskCategories.FirstOrDefault(riskCategory => riskCategory.Id == Model.RiskCategory?.Id);
SelectedRiskFeature = AvailableRiskFeatures.FirstOrDefault(riskFeature => riskFeature.Id == Model.RiskFeature?.Id);
DangerLevel = Model.DangerLevel;
Description = Model.Description;
Measures = Model.Measures;
Location = Model.Location;
Attachments = new ObservableCollection<Attachment>(model.Attachments);
isModelDirty = false;
}
}
/// <summary>
/// Add model to database
/// </summary>
[RelayCommand]
public async void CreateOrUpdate() {
CommitChanges();
if (Intent == Intent.Create) {
Model.ActivityId = ActivityId;
Model.CreationDate = Model.LastChangeDate = CreationTimeStamp;
await _unitOfWork.RiskProfiles.Add(Model);
await _unitOfWork.Save();
Model.Attachments = Attachments.ToList();
} else if (Intent == Intent.Update) {
Model.LastChangeDate = DateTime.Now;
Model.Attachments = Attachments.ToList();
_unitOfWork.RiskProfiles.Update(Model);
}
await _unitOfWork.Save();
NavigateBack();
}
[RelayCommand]
public async void AddAttachment() {
var selectedAttachment = await _popupProvider.ShowAddAttachmentPopup();
if (selectedAttachment is not null && !AttachmentAlreadyAttached(selectedAttachment)) {
Attachments.Add(selectedAttachment);
isModelDirty = true;
}
}
[RelayCommand]
public async void ShowDeleteOption(Attachment attachment) {
var result = await _popupProvider.ShowDeleteAttachmentModal();
if(result.Equals(MoreOptionsPopupResult.Delete)) {
var deleteResult = await _popupProvider.ShowDeletePopup();
if(deleteResult.Equals(DeleteDialogResult.Delete)) {
Attachments.Remove(attachment);
isModelDirty = true;
}
}
}
//...
This the the AddAttachmentModal which gets via AddAttachment():
modals:BaseModalPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:modals="clr-namespace:SiRiAs.Views.Modals"
xmlns:controls="clr-namespace:SiRiAs.Controls"
xmlns:viewmodel="clr-namespace:SiRiAs.Lib.ViewModels;assembly=SiRiAs.Lib"
xmlns:behaviors="clr-namespace:SiRiAs.Behaviors"
xmlns:selectors="clr-namespace:SiRiAs.Lib.TemplateSelectors;assembly=SiRiAs.Lib"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:models="clr-namespace:SiRiAs.Lib.Models;assembly=SiRiAs.Lib"
x:Class="SiRiAs.Views.Modals.AddAttachmentModal"
x:DataType="viewmodel:AddAttachmentModalViewModel"
xmlns:convertes="clr-namespace:SiRiAs.Lib.Converters;assembly=SiRiAs.Lib"
Title="AddAttachmentPopup">
<ContentPage.Resources>
<DataTemplate x:Key="ImageAttachmentView" x:DataType="models:Attachment">
<controls:LongPressImageButton Style="{StaticResource PreviewImageButtonStyle}"
Aspect="AspectFill"
Source="{Binding Link}"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:AddAttachmentModalViewModel}}, Path=SelectedCommand}"
CommandParameter="{Binding .}"
LongPressCommand="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:AddAttachmentModalViewModel}}, Path=ShowDeleteOptionCommand}"
LongPressCommandParameter="{Binding .}"
/>
</DataTemplate>
<DataTemplate x:Key="AudioAttachmentView" x:DataType="models:Attachment">
<controls:LongPressButton Style="{StaticResource PreviewButtonStyle}"
Text="{Binding Name}"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:AddAttachmentModalViewModel}}, Path=SelectedCommand}"
CommandParameter="{Binding .}"
LongPressCommand="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:AddAttachmentModalViewModel}}, Path=ShowDeleteOptionCommand}"
LongPressCommandParameter="{Binding .}"
/>
</DataTemplate>
//...
The AddAttachmentModalViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using SiRiAs.Lib.Models;
using SiRiAs.Lib.Repositories;
using System.Collections.ObjectModel;
using System.Diagnostics;
using SiRiAs.Lib.Services;
using SiRiAs.Lib.Helpers;
namespace SiRiAs.Lib.ViewModels {
public partial class AddAttachmentModalViewModel : ObservableObject {
[ObservableProperty]
private ObservableCollection<Attachment> attachments;
private readonly IUnitOfWork _unitOfWork;
private readonly IPopupProvider _popupProvider;
//...
public AddAttachmentModalViewModel(IUnitOfWork unitOfWork, IPopupProvider popupProvider, IAudioRecorder audioRecorder) {
_unitOfWork = unitOfWork;
_popupProvider = popupProvider;
_audioRecorder = audioRecorder;
OnSelectedAttachmentTypeChanged(SelectedAttachmentType);
}
[RelayCommand]
public async void ShowDeleteOption(Attachment attachment) {
var result = await _popupProvider.ShowDeleteAttachmentPopup();
if(result.Equals(DeleteDialogResult.Delete)) {
var deleteResult = await _popupProvider.ShowDeletePopup();
if(deleteResult.Equals(DeleteDialogResult.Delete)) {
_unitOfWork.Attachments.Remove(attachment);
await _unitOfWork.Save();
Attachments.Remove(attachment);
foreach(RiskProfile riskProfile in await _unitOfWork.RiskProfiles.GetAll()){
if(riskProfile.Attachments.Contains(attachment)) {
riskProfile.Attachments.Remove(attachment);
}
}
}
//...
}
}
Related
I have this RefreshCommand which I use to refresh a database of "notes", but when I refresh the page, instead of refreshing for 2 seconds and then just giving me the notes, it keeps on refreshing, which isn't what I would like it to do, plus it lags the app.
Here is the code
MyNotePage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:model="clr-namespace:MyApp.Models"
xmlns:mvvm="clr-namespace:MvvmHelpers;assembly=MvvmHelpers"
xmlns:viewmodels="clr-namespace:MyApp.ViewModels"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="MyApp.MyNotePage"
x:DataType="viewmodels:MyNoteViewModel"
BackgroundColor="White">
<ContentPage.BindingContext>
<viewmodels:MyNoteViewModel/>
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<xct:ItemSelectedEventArgsConverter x:Key="ItemSelectedEventArgsConverter"/>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddCommand}"/>
</ContentPage.ToolbarItems>
<ListView
BackgroundColor="Transparent"
CachingStrategy="RecycleElement"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding IsBusy, Mode=OneWay}"
ItemsSource="{Binding Note}"
RefreshCommand="{Binding RefreshCommand}"
RefreshControlColor="DarkViolet"
SelectionMode="None"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Note">
<ViewCell>
<ViewCell.ContextActions>
<MenuItem
Command="{Binding Source={Binding MyNotePage}, Path=BindingContext.RemoveCommand}"
CommandParameter="{Binding .}"
IsDestructive="True"
Text="Delete"/>
</ViewCell.ContextActions>
<Grid Padding="10">
<Frame CornerRadius="20" HasShadow="True">
<StackLayout Orientation="Horizontal">
<StackLayout VerticalOptions="Center">
<Label
FontSize="Large"
Text="{Binding Name}"
VerticalOptions="Center"/>
<Label
FontSize="Small"
Text="{Binding Id}"
VerticalOptions="Center"/>
</StackLayout>
</StackLayout>
</Frame>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
MyNotePage.xaml.cs
namespace MyApp
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MyNotePage
{
public MyNotePage()
{
InitializeComponent();
}
}
}
MyNoteViewModel.cs
namespace MyApp.ViewModels
{
public class MyNoteViewModel : ViewModelBase
{
public ObservableRangeCollection<Note> Note { get; }
public AsyncCommand RefreshCommand { get; }
public AsyncCommand AddCommand { get; }
public AsyncCommand<Note> RemoveCommand { get; }
public new bool IsBusy { get; private set; }
public MyNoteViewModel()
{
Note = new ObservableRangeCollection<Note>();
RefreshCommand = new AsyncCommand(Refresh);
AddCommand = new AsyncCommand(Add);
RemoveCommand = new AsyncCommand<Note>(Remove);
}
async Task Add()
{
var name = await App.Current.MainPage.DisplayPromptAsync("Notes", "Enter your notes here");
await NoteService.AddNote(name);
await Refresh();
}
async Task Remove(Note note)
{
await NoteService.RemoveNote(note.Id);
await Refresh();
}
async Task Refresh()
{
IsBusy = true;
await Task.Delay(2000);
Note.Clear();
var notes = await NoteService.GetNote();
Note.AddRange(notes);
IsBusy = false;
}
}
}
And NoteService.cs
namespace MyApp.Services
{
public static class NoteService
{
static SQLiteAsyncConnection db;
static async Task Init()
{
if (db != null)
return;
{
var databasePath = Path.Combine(FileSystem.AppDataDirectory, "MyData.db");
db = new SQLiteAsyncConnection(databasePath);
await db.CreateTableAsync<Note>();
}
}
public static async Task AddNote(string name)
{
await Init();
var note = new Note()
{
Name = name,
};
await db.InsertAsync(note);
}
public static async Task RemoveNote(int id)
{
await Init();
await db.DeleteAsync<Note>(id);
}
public static async Task<IEnumerable<Note>> GetNote()
{
await Init();
var note = await db.Table<Note>().ToListAsync();
return note;
}
}
}
Maybe it is something to do with IsBusy? I don't know if I set that up properly. The bool does change from false to true but it looks like it doesn't do the opposite to stop the refresh. Thanks for the help.
I pointed this out to you in your previous question. IsBusy does not raise a PropertyChanged event so the UI is never notified
I have this piece of code and I don't even know how to initialize it at this point. I tried a few different versions but all of them come up with a warning saying
"Warning CS0649 Field 'NoteService.db' is never assigned to, and will always have its default value null"
The part where I try to create this table looks like this:
static SQLiteAsyncConnection db;
static async Task Init()
{
if (db != null)
return;
{
var databasePath = Path.Combine(FileSystem.AppDataDirectory, "MyData.db");
db = new SQLiteAsyncConnection(databasePath);
await db.CreateTableAsync<Note>();
}
}
This is the entirety of the code
MyNoteViewModel.cs:
namespace MyApp.ViewModels {
public class MyNoteViewModel : ViewModelBase
{
public ObservableRangeCollection<Note> Note { get;}
public AsyncCommand RefreshCommand { get; }
public AsyncCommand AddCommand { get; }
public AsyncCommand<Note> RemoveCommand { get; }
public new bool IsBusy { get; private set; }
public MyNoteViewModel()
{
Note = new ObservableRangeCollection<Note>();
RefreshCommand = new AsyncCommand(Refresh);
AddCommand = new AsyncCommand(Add);
RemoveCommand = new AsyncCommand<Note>(Remove);
}
async Task Add()
{
var name = await App.Current.MainPage.DisplayPromptAsync("Notes", "Enter your notes here");
await NoteService.AddNote(name);
await Refresh();
}
async Task Remove(Note note)
{
await NoteService.RemoveNote(note.Id);
await Refresh();
}
async Task Refresh()
{
IsBusy = true;
await Task.Delay(2000);
Note.Clear();
var notes = NoteService.GetNote();
Note.AddRange((IEnumerable<Note>)notes);
IsBusy = false;
return;
}
NotePage.xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:model="clr-namespace:MyApp.Models"
xmlns:mvvm="clr-namespace:MvvmHelpers;assembly=MvvmHelpers"
xmlns:viewmodels="clr-namespace:MyApp.ViewModels"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="MyApp.MyNotePage"
x:Name="MyNotePage"
x:DataType="viewmodels:MyNoteViewModel"
BackgroundColor="White">
<ContentPage.BindingContext>
<viewmodels:MyNoteViewModel/>
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<xct:ItemSelectedEventArgsConverter x:Key="ItemSelectedEventArgsConverter"/>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddCommand}"/>
</ContentPage.ToolbarItems>
<ListView
BackgroundColor="Transparent"
CachingStrategy="RecycleElement"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding IsBusy, Mode=OneWay}"
ItemsSource="{Binding Note}"
RefreshCommand="{Binding RefreshCommand}"
RefreshControlColor="DarkViolet"
SelectionMode="None"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Note">
<ViewCell>
<ViewCell.ContextActions>
<MenuItem
Command="{Binding Source={x:Reference MyNotePage}, Path=BindingContext.RemoveCommand}"
CommandParameter="{Binding .}"
IsDestructive="True"
Text="Delete"/>
</ViewCell.ContextActions>
<Grid Padding="10">
<Frame CornerRadius="20" HasShadow="True">
<StackLayout Orientation="Horizontal">
<StackLayout VerticalOptions="Center">
<Label
FontSize="Large"
Text="{Binding Name}"
VerticalOptions="Center"/>
<Label
FontSize="Small"
Text="{Binding Id}"
VerticalOptions="Center"/>
</StackLayout>
</StackLayout>
</Frame>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
NotePage.xaml.cs:
namespace MyApp
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MyNotePage
{
public MyNotePage()
{
InitializeComponent();
}
}
}
NoteService.cs:
namespace MyApp.Services
{
public static class NoteService
{
static SQLiteAsyncConnection db;
static async Task Init()
{
if (db != null)
return;
{
var databasePath = Path.Combine(FileSystem.AppDataDirectory, "MyData.db");
db = new SQLiteAsyncConnection(databasePath);
await db.CreateTableAsync<Note>();
}
}
public static async Task AddNote(string name)
{
await Init();
var note = new Note()
{
Name = name,
};
var id = await db.InsertAsync(note);
}
public static async Task RemoveNote(int id)
{
await Init();
await db.DeleteAsync<Note>(id);
}
public static async Task<IEnumerable<Note>> GetNote()
{
await Init();
var note = await db.Table<Note>().ToListAsync();
return note;
}
}
}
GetNote is async, but you are not using await when calling it. That means that notes is a Task. Then when you attempt to cast it, the cast fails and returns null, which then causes a NullRef exception
var notes = NoteService.GetNote();
Note.AddRange((IEnumerable<Note>)notes);
instead do this
var notes = await NoteService.GetNote();
Note.AddRange(notes);
var db = new SQLiteAsyncConnection(databasePath);
You are creating a new field scoped to the method, which is allowed by the compiler, but not what you want. Remove the var keyword to reference your class-level field.
I am currently working on a simple task management MVVM application.
Minimal code of the project:
MainPage.xaml
<Page x:Class="TestArea.MainPage"
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="using:TestArea"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<ListView x:Name="TasksListView"
Grid.Row="0"
ItemsSource="{x:Bind ViewModel.Tasks, Mode=OneWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Loaded">
<core:EventTriggerBehavior.Actions>
<core:InvokeCommandAction
Command="{x:Bind ViewModel.OnLoadedCommand, Mode=OneWay}" />
</core:EventTriggerBehavior.Actions>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Task">
<Border HorizontalAlignment="Stretch"
BorderBrush="Gray"
BorderThickness="1"
Background="White"
MinHeight="55">
<CheckBox VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Content="{Binding Description, Mode=OneWay}"
IsChecked="{Binding IsCompleted, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Checked">
<core:EventTriggerBehavior.Actions>
<core:InvokeCommandAction
Command="{Binding ElementName=TasksListView, Path=DataContext.OnCheckedTaskCommand}"
CommandParameter="{x:Bind Mode=OneWay}" />
</core:EventTriggerBehavior.Actions>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</CheckBox>
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page>
MainPage.xaml.cs
namespace TestArea;
public sealed partial class MainPage
{
public MainPage()
{
InitializeComponent();
ViewModel = new MainPageViewModel();
}
public MainPageViewModel ViewModel
{
get => (MainPageViewModel) DataContext;
private set => DataContext = value;
}
}
MainPageViewModel.cs
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Windows.UI.Xaml;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
namespace TestArea;
public class MainPageViewModel : ObservableObject
{
private DataContext _db;
public MainPageViewModel()
{
_db = new DataContext();
// Add items in db
for (int i = 1; i <= 10; i++)
{
var newTask = new Task
{
Id = i,
Description = $"Task-{i}",
IsCompleted = false
};
_db.Tasks.Add(newTask);
}
_db.SaveChanges();
Refresh();
OnLoadedCommand = new RelayCommand<RoutedEventArgs>(OnLoaded);
OnCheckedTaskCommand = new RelayCommand<Task>(OnCheckedTask);
}
private void Refresh()
{
Tasks = _db.Tasks.ToList();
}
private List<Task> _tasks;
public List<Task> Tasks
{
get => _tasks;
set => SetProperty(ref _tasks, value);
}
public ICommand OnLoadedCommand { get; }
private void OnLoaded(RoutedEventArgs e)
{
// Get data from db
Refresh();
}
public ICommand OnCheckedTaskCommand { get; }
private void OnCheckedTask(Task task)
{
// Find in db task with id == task.Id
var suitableTaskFromDb = _db.Tasks.First(taskInDb => taskInDb.Id == task.Id);
if (suitableTaskFromDb != null)
{
// Mark as completed
suitableTaskFromDb.IsCompleted = true;
// Save changes and refresh view
_db.SaveChanges();
Refresh();
}
}
}
TaskModel.cs
namespace TestArea;
public class Task
{
public int Id { get; set; }
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
DataContext.cs
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
namespace TestArea;
public sealed class DataContext : DbContext
{
public DataContext()
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
DbPath = Path.Combine(path, "TestApp.db");
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlite($"Data Source={DbPath}");
}
public string DbPath { get; }
public DbSet<Task> Tasks { get; set; }
}
References:
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Tools
Microsoft.NETCore.UniversalWindowsPlatform
Microsoft.Toolkit.Mvvm
Microsoft.UI.Xaml.Markup
Microsoft.Xaml.Behaviors.Uwp.Managed
Universal Windows
To store data I am using SQLite database and Entity Framework library.
In the UI part I have a ListView which contains tasks that are presented as CheckBox controls. If the task is completed - you need to check the checkbox. Loading the records themselves from the database seems to be in okay, I can see the created records in ListView. However, the problem arises with the IsChecked property and saving/reading from the database IsCompleted parameter. At least I understand that, because the problem came after I added OnCheckedTaskCommand.
When I click on the checkbox, I get the following exception:
System.Exception: 'No installed components were detected.
Child collection must not be modified during measure or arrange.'
This exception was originally thrown at this call stack:
[External Code]
TestArea.MainPageViewModel.Tasks.set(System.Collections.Generic.List<TestArea.Task>) in MainPageViewModel.cs
TestArea.MainPageViewModel.Refresh() in MainPageViewModel.cs
TestArea.MainPageViewModel.OnCheckedTask(TestArea.Task) in MainPageViewModel.cs
[External Code]
What could be the cause?
I need help trying to figure out why my collection-view is not displaying the data that its binded to. When I run the application in debug mode the data is being populated into the Viewmodel and binded. When I go to the View.xaml and hover over the source where its binded, it displays.
I have provided the Model, ModelView, View and the code behind for the View and even a screen shot of the view running in the debugger showing that the bind seems to be working.
I have been stuck for a while any help will be truly appreciated.
What I see when I run in debug mode showing the view model is binded but just not showing.
ContactsPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Project_contact.Views.ItemsPage"
Title="{Binding Title}"
x:Name="BrowseItemsPage">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Clicked="AddItem_Clicked" />
</ContentPage.ToolbarItems>
<RefreshView IsRefreshing="{Binding IsBusy, Mode=TwoWay}" Command="{Binding LoadDataCommand}">
<StackLayout>
<Label x:Name="TopBanner" Text="Welcome Please wait..." />
<StackLayout Orientation="Horizontal">
<Label Text= "{Binding StringFormat='Welcome You have' }" />
</StackLayout>
<CollectionView x:Name="ItemsCollectionView2"
ItemsSource="{Binding Contacts}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<Label Text="{Binding desc}"
d:Text="Item descripton"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
FontSize="13" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Tapped="OnContactSelected_Tapped"></TapGestureRecognizer>
<SwipeGestureRecognizer Direction="Left" />
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</RefreshView>
</ContentPage>
ContactsPage.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Project_Contact.Models;
using Project_Contact.Views;
using Project_Contact.ViewModels;
using Project_Contact.Services;
using System.Data;
namespace Project_Contact.Views
{
// Learn more about making custom code visible in the Xamarin.Forms previewer
// by visiting https://aka.ms/xamarinforms-previewer
[DesignTimeVisible(false)]
public partial class ItemsPage : ContentPage
{
public ContactsViewModel viewModel { get; set; }
public ContactStore contactStore { get; set; }
public ContactsPage()
{
contactStore = new ContactStore(DependencyService.Get<Database>());
viewModel = new ContactsViewModel(contactStore);
viewModel.LoadDataCommand.Execute(null);
BindingContext = viewModel;
InitializeComponent();
}
async void OnItemSelected(object sender, EventArgs args)
{
var layout = (BindableObject)sender;
var item = (Item)layout.BindingContext;
await Navigation.PushAsync(new ItemDetailPage(new ItemDetailViewModel(item)));
}
async void AddItem_Clicked(object sender, EventArgs e)
{
await Navigation.PushModalAsync(new NavigationPage(new NewContactPage(contactStore)));
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.LoadDataCommand.Execute(true);
}
async void OnContactSelected_Tapped(object sender, EventArgs e)
{
var layout = (BindableObject)sender;
var contact = (Contact)layout.BindingContext;
await Navigation.PushAsync(new ContactDetailPage(new ContactDetailViewModel(contactStore,contact)));
}
}
}
ContactsPageViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
using Project_contact.Models;
using Project_contact.Views;
using Project_contact.Services;
namespace Project_contact.ViewModels
{
public class ContactsViewModel : BaseViewModel
{
public ObservableCollection<Contact> Contacts { get; set; } = new ObservableCollection<Contact>();
public Command LoadContacts { get; private set; }
public Command LoadDataCommand { get; private set; }
// public Command load
public ContactStore contactStore;
public int numberofContacts { get; set; }
public string TopBannerText { get; set; }
public ContactsViewModel(ContactStore contactStore)
{
Title = "Browse";
this.contactStore = contactStore;
LoadDataCommand = new Command(async () => await ExecuteLoadDataCommand());
}
public async Task ExecuteLoadDataCommand()
{
Contacts = new ObservableCollection<Contact>(await contactStore.GetContactsAsync());
LoadContacts = new Command(async () => await ExecuteLoadContactsCommand());
TopBannerText = String.Format("Welcome you have {0} contacts ",numberofContacts);
}
async Task ExecuteLoadContactsCommand()
{
if (!IsBusy)
{
IsBusy = true;
try
{
if (Contacts.Count > 0)
{
Contacts.Clear();
numberofContacts = 0;
}
var contacts = await contactStore.GetContactsAsync();
foreach (var contact in contacts)
{
Contacts.Add(contact);
numberofContacts++;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
}
}
Contact.cs
using System;
using System.Collections.Generic;
using System.Text;
using SQLite;
namespace Project_Contact.Models
{
public class Contact
{
[PrimaryKey, AutoIncrement]
public int id { get; set; }
public string name { get; set; }
public string desc { get; set; }
public string number {get; set;}
}
}
I want to delete item from ListView and ViewModel ObservableRangeCollection in Xamrin.Form.
EmployeeResultsPage
<ListView x:Name="EmployeeResultsListView"
ItemsSource="{Binding EmployeeResults}"
RowHeight="200"
IsPullToRefreshEnabled="true"
RefreshCommand="{Binding RefreshDataCommand}"
IsRefreshing="{Binding IsRefreshingData, Mode=OneWay}"
ItemAppearing="Employee_ItemAppearing"
<ListView.ItemTemplate>
<DataTemplate>
<local:EmployeeResultViewCell />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
EmployeeResultViewModel
[ImplementPropertyChanged]
public class EmployeeResultsViewModel : ViewModelBase
{
private async Task LoadEmployee()
{
EmployeeResults = GetDataUsingAPI(); //15 records per call
OnDeleteEmployeeCommand = new RelayCommand<object>(async (model) => await DeleteEmployee(model));
}
public ObservableRangeCollection<ExtendedEmployee> EmployeeResults { get; set; }
public string EmployeePhotoUrl { get; set; }
public string EmployeeName { get; set; }
public ICommand OnDeleteMessageCommand { get; set; }
private async Task DeleteEmployee(object obj)
{
//get object here
}
}
EmployeeResultViewCell
<ViewCell.ContextActions>
<MenuItem
Text="Delete"
IsDestructive="true"
Command="{Binding Path=BindingContext.OnDeleteEmployeeCommand, Source={x:Reference EmployeeResultsPage}}"
CommandParameter="{Binding .}"/>
</ViewCell.ContextActions>
<ViewCell.View>
<Grid RowSpacing="0" ColumnSpacing="0" VerticalOptions="Center" HeightRequest="150">
<Image x:Name="EmployeeImage" Grid.Column="0" HeightRequest="150" WidthRequest="150"
Source="{Binding EmployeePhotoUrl}" />
<Label Text="{Binding EmployeeName}" FontSize="18" TextColor="Grey"/>
</Grid>
</ViewCell.View>
CS File
public partial class EmployeeResultViewCell : CustomViewCell
{
public EmployeeResultViewCell()
{
InitializeComponent();
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
var employee = (BindingContext as ExtendedEmployee);
}
}
Changed: Remove click event and add binding.
With MVVM you are not using events, but rather commands in your viewmodels that you can bind to commands of your views. The most MVVM way to achieve what you want would be to add a ICommand bindable property to your cell
public partial class EmployeeResultViewCell : CustomViewCell
{
/// <summary>
/// The <see cref="DeleteCommand" /> bindable property.
/// </summary>
public static readonly BindableProperty DeleteCommandProperty = BindableProperty.Create(nameof(SealCheckListPage.DeleteCommand), typeof(ICommand), typeof(SealCheckListPage), default(ICommand));
/// <summary>
/// Gets or sets the DeleteCommand that is called when we'd like to delete an employee.
/// </summary>
public ICommand DeleteCommand
{
get => (ICommand)this.GetValue(SealCheckListPage.DeleteCommandProperty);
set => this.SetValue(SealCheckListPage.DeleteCommandProperty, value);
}
private void MenuItemDelete_Clicked(object sender, System.EventArgs e)
{
DeleteCommand?.Execute(BindingContext);
}
}
Now you can bind the DeleteCommand to your viewmodel
<ListView>
<ListView.ItemTemplate>
<DataTemplate>
<local:EmployeeResultViewCell DeleteCommand="{Binding BindingContext.DeleteCommand, Source={x:Reference Page}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Please note that the page that contains the ListView must have x:Name="Page" in order to bind the command correctly. Admittedly, binding the command that way is not really optimal, but as far as I know this is the best we can do, MVVM-wise.
The last thing you'll have to do is to add an ICommand property to your viewmodel
public class EmployeeResultsViewModel : ViewModelBase
{
private async Task LoadEmployee()
{
EmployeeResults = GetDataUsingAPI(); //15 records per call
DeleteCommand = new Command<ExtendedEmployee>(OnDelete);
}
public ICommand DeleteCommand { get; }
private void OnDelete(ExtendedEmployee employee)
{
// delete the employee from the collection
}
/* ... */
}
Now, when your cell receives the event, it executes the command, which is bound to the command in your viewmodel. When this command is executed, it executes the passed delegate OnDelete in which you have access to the collection you'd like to delete the ExtendedEmployee from.