Reputation: 184
I'm trying to build this simple UWP MVVM notetaking app.
The purpose of the app is adding text from a Textbox
to a ListView
, when a Add Button
is clicked, or delete an item of the ListView
by pressing a Delete Button
, which is assigned to each item in the ListView
.
Adding items to the ObservableCollection<Note>
seems to work fine. The items are shown in the ListView
without any problems.
Deleting item doesn't work as it should.
I tried to invoke the method responsible for deleting the items from both the constructor and the Delete Button
.
When I invoke DoDeleteNote(Note itemToDelete)
from the Delete Button
, nothing happens, but if i invoke the same method from the constructor then the item is deleted.
I created a breakpoint in the DoDeleteNote(Note itemToDelete)
method, and I can see in the debugger that it runs through the code, but nothing is deleted from the ObservableCollection<Note>
.
However when i invoke the DoDeleteNote(Note itemToDelete)
method from the constructor the item is deleted.
It is also strange, that the Note items I create and add to the ObservableCollection<Note>
from the NoteViewModel constructor are the only items that are in the ObservableCollection<Note>
. The items I add using the Add Button
, are gone, but still shown in the ListView
.
I’m thinking maybe something is wrong with either INotifyPropertyChanged
or the bindings, but i’m not sure where to start looking, and what to look for, so I could use some help.
I know there seems to be a lot of code here, but I felt it was necessary not to omit anything to understand the data flow.
<Page
x:Class="ListView2.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ListView2"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModel="using:ListView2.ViewModel"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.DataContext>
<viewModel:NoteViewModel/>
</Grid.DataContext>
<ListView Header="Notes"
HorizontalAlignment="Left"
Height="341"
Width="228"
VerticalAlignment="Top"
Margin="163,208,0,0"
ItemsSource="{Binding Notes, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemTemplate>
<DataTemplate x:Name="MyDataTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TbxblListItem" Text="{Binding NoteText}"/>
<Button Command="{Binding DeleteNoteCommand}"
CommandParameter="{Binding ElementName=TbxblListItem}">
<Button.DataContext>
<viewModel:NoteViewModel/>
</Button.DataContext>
<Button.Content>
<SymbolIcon Symbol="Delete"
ToolTipService.ToolTip="Delete Note"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button.Content>
</Button>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBox x:Name="TbxNoteContent" HorizontalAlignment="Left"
Margin="571,147,0,0"
TextWrapping="Wrap"
VerticalAlignment="Top"
Width="376"/>
<Button Content="Add"
HorizontalAlignment="Left"
Margin="597,249,0,0"
VerticalAlignment="Top"
Command="{Binding AddNoteCommand}"
CommandParameter="{Binding Text, ElementName=TbxNoteContent}"/>
</Grid>
</page>
namespace ListView2.Model
{
class Note
{
private string _noteText;
public Note(string noteText)
{
NoteText = noteText;
}
public string NoteText { get { return _noteText; } set { _noteText = value; } }
}
}
using System.ComponentModel;
namespace ListView2.Model
{
class Notification : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
using System.Collections.ObjectModel;
using Windows.UI.Xaml.Controls;
using ListView2.Model;
namespace ListView2.ViewModel
{
class NoteViewModel : Notification
{
#region Instance Fields
private ObservableCollection<Note> _notes;
private RelayCommand _addNoteCommand;
private RelayCommand _deleteNoteCommand;
//private string _noteText;
#endregion
#region Constructors
public NoteViewModel()
{
//adds sample data to Notes property (ObservableCollection<Note>)
Notes = new ObservableCollection<Note>() { new Note("Sample text 1"), new Note("Sample text 2") };
//Used for testing the deletion of items from ObservableCollection-------------------------------------
Notes.RemoveAt(1);
Notes.Add(new Note("Sample text 3"));
//foreach (var item in Notes)
//{
// if (item.NoteText == "Sample text 3")
// {
// DoDeleteNote(item);
// break;
// }
//}
//------------------------------------------------------
//Button command methods are added to delegates
AddNoteCommand = new RelayCommand(DoAddNote);
DeleteNoteCommand = new RelayCommand(DoDeleteNote);
}
#endregion
#region Properties
public ObservableCollection<Note> Notes { get { return _notes; } set { _notes = value; OnPropertyChanged("Notes"); } }
//public string NoteText { get { return _noteText; } set { _noteText = value; OnPropertyChanged("NoteText"); } }
public RelayCommand AddNoteCommand { get { return _addNoteCommand; } set { _addNoteCommand = value; } }
public RelayCommand DeleteNoteCommand { get { return _deleteNoteCommand; } set { _deleteNoteCommand = value; } }
#endregion
#region methods
private void DoAddNote(object obj)
{
var newItem = obj as string;
if (!string.IsNullOrEmpty(newItem))
{
AddNote(newItem);
}
}
//Work in progress
private void DoDeleteNote(object obj)
{
//Used when the XAML Delete Button invokes this method
TextBlock textBlockSender = obj as TextBlock;
//string myString = textBlockSender.Text;
Note itemToDelete = textBlockSender.DataContext as Note;
//Used when the constuctor invokes this method, for testing purposes------------
//Note itemToDelete = obj as Note;
//--------------------------------------------------------
foreach (Note note in this.Notes)
{
if (note.NoteText == itemToDelete.NoteText)
{
//int noteIndex = Notes.IndexOf(note);
//Notes.RemoveAt(noteIndex);
DeleteNote(note);
break;
}
}
//if (Notes.Contains(itemToDelete))
//{
// Notes.Remove(itemToDelete);
//}
}
public void AddNote(string noteText)
{
this.Notes.Add(new Note(noteText));
}
public void DeleteNote(Note itemToDelete)
{
this.Notes.Remove(itemToDelete);
}
#endregion
}
}
RelayCommand class which is the implementation for ICommand does not seem relevant for the question, so I haven't included it here, but if you are curious it can be seen on GitHub
Upvotes: 0
Views: 812
Reputation: 3612
As @Eugene Podskal points out one of the problems is with this code
<Button.DataContext>
<viewModel:NoteViewModel/>
</Button.DataContext>
Your Layout Grid instantiates a new NoteViewModel
and the above code will do the same leaving you with 2 NoteViewModels active on the page.
Firstly give the ListView a name
<ListView x:Name="MyList" Header="Notes"
Next let's fix the Bindings on your DataTemplate
of ListView MyList
<ListView.ItemTemplate>
<DataTemplate x:Name="MyDataTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TbxblListItem" Text="{Binding NoteText}"/>
<Button Command="{Binding DataContext.DeleteNoteCommand, ElementName=MyList}"
CommandParameter="{Binding}">
<SymbolIcon Symbol="Delete"
ToolTipService.ToolTip="Delete Note"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
this line
Command="{Binding DataContext.DeleteNoteCommand, ElementName=MyList}"
means we are now Binding to the DataContext
of MyList
which is your NoteViewModel
as defined under the main Grid
The CommandParamter
is simplified to
CommandParameter="{Binding}"
as I explain below it is better practice to Bind to the object in your MyList
which in this case is an object of Note
To make this work we need to adjust your NoteViewModel
slightly
Change your private delete field and public property to
private RelayCommand<Note> _deleteNoteCommand;
public RelayCommand<Note> DeleteNoteCommand { get { return _deleteNoteCommand; } set { _deleteNoteCommand = value; } }
and in the constructor
DeleteNoteCommand = new RelayCommand<Note>(DoDeleteNote);
DoDeleteNote
method is simplified to
private void DoDeleteNote(Note note)
{
this.Notes.Remove(note);
}
so we handily get rid of casting from a TextBlock
. You can now get rid of the DeleteNote
method as it's no longer needed.
Finally we need to add a new RelayCommand
that takes a Generic Type to make our Command DeleteNoteCommand
work correctly.
RelayCommand
public class RelayCommand<T> : ICommand
{
#region Fields
private readonly Action<T> _execute = null;
private readonly Predicate<T> _canExecute = null;
#endregion
#region Constructors
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command with conditional execution.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<T> execute, Predicate<T> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
CanExecuteChanged(this, new EventArgs());
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
#endregion
}
Apologies this answer is long but I wanted to point out every step. I also suggest using a mvvm framework when working with xaml as it will make your life alot easier. I recommend mvvmlight but there are plenty of others. Hope that helps
Upvotes: 1