Custom bindable stacklayout should trigger UI when item of ObservableCollection change - c#

I have a problem with my custom stacklayout which populates the stacklayout correctly but does not recognizes any changes of any item in the bound observable collection..
This is the code I use for the bindable stacklayout:
public class BindableStackLayout : StackLayout
{
private readonly Label _header;
public BindableStackLayout()
{
_header = new Label();
Children.Add(_header);
}
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable),
typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateItems());
public DataTemplate ItemDataTemplate
{
get => (DataTemplate)GetValue(ItemDataTemplateProperty);
set => SetValue(ItemDataTemplateProperty, value);
}
public static readonly BindableProperty ItemDataTemplateProperty = BindableProperty.Create(nameof(ItemDataTemplate),
typeof(DataTemplate), typeof(BindableStackLayout));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string),
typeof(BindableStackLayout), propertyChanged: (bindable, oldValue, newValue) => ((BindableStackLayout)bindable).PopulateHeader());
private void PopulateItems()
{
if (ItemsSource == null)
return;
foreach (var item in ItemsSource)
{
var itemTemplate = ItemDataTemplate.CreateContent() as Xamarin.Forms.View;
itemTemplate.BindingContext = item;
Children.Add(itemTemplate);
}
}
private void PopulateHeader() => _header.Text = Title;
}
Which is used like you can find here:
<ContentView.Content>
<h:BindableStackLayout ItemsSource="{Binding MenuHotKeys, Mode=TwoWay}"
Style="{StaticResource MenuControlStackLayout}">
<h:BindableStackLayout.ItemDataTemplate>
<DataTemplate>
<Button Text="{Binding DataA}"
Command="{Binding Path=BindingContext.MenuControlCommand, Source={x:Reference InternalMenuControl}}"
CommandParameter="{Binding .}"
Style="{StaticResource MenuControlButton}"/>
</DataTemplate>
</h:BindableStackLayout.ItemDataTemplate>
</h:BindableStackLayout>
</ContentView.Content>
And in the viewmodel I have this code:
private ObservableCollection<ConfigMenuItem> _menuHotKeys;
public ObservableCollection<ConfigMenuItem> MenuHotKeys
{
get => _menuHotKeys;
set => SetValue(ref _menuHotKeys, value);
}
And the change is here:
private async void MenuControlButtonPressed(object sender)
{
var menuItem = sender as ConfigMenuItem;
if (menuItem.ItemId == _expanderId)
{
// toggle expanded menu visibility
var expander = _menuHotKeys.FirstOrDefault(p => p.ItemId == _expanderId);
var buffer = expander.DataA;
expander.DataA = expander.DataB;
expander.DataB = buffer;
}
else
{
await NavigationHandler.NavigateToMenuItem(menuItem);
}
}
As you can see, I want to toggle the name of the bound button, but the changes does not appear.
I think I have to change something in bindable stacklayout class, but what?
Maybe you can help
#INPC answers:
The ConfigMenuItem in the Collection derives from:
public abstract class BaseObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SetValue<T>(ref T field, T value, Expression<Func<T>> property)
{
if (!ReferenceEquals(field, value))
{
field = value;
OnPropertyChanged(property);
}
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> changedProperty)
{
if (PropertyChanged != null)
{
string name = ((MemberExpression)changedProperty.Body).Member.Name;
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
and the viewmodel derives from:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void SetValue<T>(ref T privateField, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(privateField, value))
{
privateField = value;
OnPropertyChanged(propertyName);
}
return;
}
}
As requested in comments the ConfigMenuItem class, code of BaseObject see in the code upside:
public class ConfigMenuItem : BaseObject, IConfigMenuItem
{
public int ItemId
{
get;
set;
}
public int Position
{
get;
set;
}
public string Name
{
get;
set;
}
public string DataA
{
get;
set;
}
public string DataB
{
get;
set;
}
public bool IsEnabled
{
get;
set;
}
public bool IsHotKey
{
get;
set;
}
public bool IsCustomMenuItem
{
get;
set;
}
public override string ToString()
{
return $"{Name} ({DataA} | {DataB ?? "null"})";
}
}

The problem is caused by the fact that although your ConfigMenuItem class derives from BaseObject, all its properties are plain properties and do not tirgger PropertyChanged event. You have to rewrite the properties to have a backing field and to trigger the event in their setter. For example:
private string _dataA;
public string DataA
{
get => _dataA;
set => SetValue(ref _dataA, value);
}
My example is using the SetValue method from BaseViewModel, and I actually think the BaseObject class is redundant and you could just use BaseViewModel instead. Using [CallerMemberName] for property is much more convenient than having additional logic for Expression<Func<T>>.

Related

Deleting and Editing an item in a listview with Viewmodel and without using the codebehind

In the shown code i need to know the coding to be replaced in place of question mark in the code. I need to delete,edit and update the item in the list view without writing any code in code behind. I only want to do these operations by bindin view with view model through Icommand
This a class in my model Playlist.cs
namespace MvvmDemo.Models
{
public class Playlist
{
public string Title { get; set; }
}
}
This is a class in my viewmodel PlaylistsViewModel.cs
namespace MvvmDemo.ViewModels
{
public class PlaylistsViewModel
{
public ObservableCollection Playlists { get; private set; } = new ObservableCollection();
public ICommand AddPlaylistCommand { get; private set; }
public ICommand DeletePlaylistCommand { get; private set; }
public ICommand EditPlaylistCommand { get; private set; }
public PlaylistsViewModel()
{
AddPlaylistCommand = new Command(AddPlaylist);
DeletePlaylistCommand = new Command(DeletePlaylist);
}
public void AddPlaylist()
{
var newPlaylist = "Playlist " + (Playlists.Count + 1);
Playlists.Add(new Playlist { Title = newPlaylist });
}
public void DeletePlaylist()
{
????????????????
}
public void EditPlaylist()
{
????????????????
}
}
}
you have to make the command is parameterised and pass binding data through the parameter.
and from that data you can get the index value of selected.using that remove the item from the list.
Playlists.RemoveAt("INDEX_NUMBER");
To update it in the view use "INotifyProperty" also
If you want to delete and edit item in ListView, firstly, you should need to use ICommand, then you could need to use INotifyPropertyChanged to implement Inotify.
I do one sample that you can take a look. Choosing one Item and long press with the left mouse button, you will see two ways, delete Item and Edit Item action.
<ContentPage.Content>
<StackLayout>
<ListView
x:Name="mylistview"
ItemsSource="{Binding lists}"
SelectedItem="{Binding selecteditem}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.ContextActions>
<MenuItem
Command="{Binding BindingContext.DeletePlaylistCommand, Source={x:Reference Name=mylistview}}"
IsDestructive="true"
Text="Delete Item" />
<MenuItem
Command="{Binding BindingContext.EditPlaylistCommand, Source={x:Reference Name=mylistview}}"
IsDestructive="true"
Text="Edit Item" />
</ViewCell.ContextActions>
<StackLayout Padding="15,0">
<Label Text="{Binding Title}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class Page19 : ContentPage, INotifyPropertyChanged
{
public ObservableCollection<Playlist> lists { get; set; }
//public RelayCommand1 AddPlaylistCommand { get; set; }
public RelayCommand DeletePlaylistCommand { get; set; }
public RelayCommand EditPlaylistCommand { get; set; }
private Playlist _selecteditem;
public Playlist selecteditem
{
get { return _selecteditem; }
set
{
_selecteditem = value;
RaisePropertyChanged("selecteditem");
}
}
public Page19 ()
{
InitializeComponent ();
lists = new ObservableCollection<Playlist>()
{
new Playlist(){Id=1,Title="list 1"},
new Playlist(){Id=2, Title="list 2"},
new Playlist(){Id=3,Title="list 3"},
new Playlist(){Id=4,Title="list 4"},
new Playlist(){Id=5,Title="list 5"},
new Playlist(){Id=6,Title="list 6"},
};
DeletePlaylistCommand = new RelayCommand(DeletePlaylist);
EditPlaylistCommand = new RelayCommand(EditPlaylist);
selecteditem = lists[0];
this.BindingContext = this;
}
public void AddPlaylist()
{
}
public void DeletePlaylist()
{
Playlist item = selecteditem;
lists.Remove(item);
}
public void EditPlaylist()
{
Playlist item = selecteditem;
int id = item.Id;
foreach(Playlist playl in lists.Where(a=>a.Id==id))
{
playl.Title = "chenge title";
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class Playlist: INotifyPropertyChanged
{
private int _Id;
public int Id
{
get { return _Id; }
set
{
_Id = value;
RaisePropertyChanged("Id");
}
}
private string _Title;
public string Title
{
get { return _Title;}
set
{
_Title = value;
RaisePropertyChanged("Title");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Here is the RelayCommd:
public class RelayCommand : ICommand
{
readonly Action _execute;
public RelayCommand(Action execute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_execute();
}
}
You can use observablecollection. It will reflect add,remove operation of item to the listview. And for editing item you have to raise property changed for all property you are editing.To simplify that property changed you can implement property changed event to your Playlist model class.
Like
public void DeletePlaylist()
{
Playlists.Remove(newPlaylist);
}
public void EditPlaylist()
{
newPlaylist.Title="Refreshed Playlist"
}
public class Playlist:INotifyPropertyChanged
{
private string title;
public string Title
{
get{return title;}
set{title=value;
NotifyPropertyChanged();}
}
}

How to work with a model in viewModel

I'm having a problem with using MVVM for a Xamarin project.
I can not refresh the user interface if one of my objects in my viewModel is updated (after a PUT request, for example).
Let me explain :
My model :
public class MyObject
{
public string Id { get; private set; }
public string Name { get; private set; }
}
My viewmodel :
public class MyViewModel : BaseViewModel
{
public MyObject MyObject { get; private set; }
public string IdMvvm
{
set
{
if (this.MyObject.Id != value)
{
MyObject.Id = value;
OnPropertyChanged(nameof(IdMvvm));
}
}
get { return MyObject.Id; }
}
public string NameMvvm
{
set
{
if (this.MyObject.Name != value)
{
MyObject.Name = value;
OnPropertyChanged(nameof(NameMvvm));
}
}
get { return MyObject.Name; }
}
}
BaseViewModel implements INotifyPropertyChanged
public class BaseViewModel : INotifyPropertyChanged
{
public string PageTitle { get; protected set; }
LayoutViewModel() {}
// MVVM ----------------------------------------------------------------------------------------
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
return;
backingField = value;
OnPropertyChanged(propertyName);
}
MyViewModel is defined as BindingContext for my page
My properties IdMvvm and NameMvvm are bind in Entry in my page in xaml
When I modify an Entry then the value is raised but if my MyModel object changes value, for example update (click on a button) then the value of the different Entry is not updated
Can you help me please? Because it seems that I missed something ...
If you need more explanation, tell me to know
Sorry if my english is not good
It is because when you change the model, your view is not aware about the change. Update your code so that you explicitly notify property changes when your model changes.
public class MyViewModel : BaseViewModel
{
private MyObject _myObject;
public MyObject MyObject
{
get { return _myObject; }
private set { _myObject = value; NotifyModelChange(); }
}
public string IdMvvm
{
set
{
if (this.MyObject.Id != value)
{
MyObject.Id = value;
OnPropertyChanged(nameof(IdMvvm));
}
}
get { return MyObject.Id; }
}
public string NameMvvm
{
set
{
if (this.MyObject.Name != value)
{
MyObject.Name = value;
OnPropertyChanged(nameof(NameMvvm));
}
}
get { return MyObject.Name; }
}
private void NotifyModelChange()
{
OnPropertyChanged(nameof(IdMvvm));
OnPropertyChanged(nameof(NameMvvm));
}
}

Binding TextBox to object WPF

I'm trying to make the TextBox show the stringBody property of the CurrentDialog property of the window. Here's the XAML part:
<TextBox x:Name="ComposerBox" Height="90.302" Margin="311,0,141.355,10"
...
Text="{Binding Body}"
ScrollViewer.CanContentScroll="True" SpellCheck.IsEnabled="True"
VerticalAlignment="Bottom">
Here is a string from the windows constructor:
MessagingBox.DataContext = CurrentDialog;
I've also tried to set DataContext to this with no result.
Here's how CurrentDialog is defined:
private MessageDialog CurrentDialog { get; set; }
Here's the MessageDialog class definition:
[Serializable][DataContract]
public class MessageDialog
{
public string Name { get; private set; }
public UserData User { get; private set; }
private List<Message> Dialog = new List<Message>();
public string Body { get; private set; }
public MessageDialog(UserData data)
{
Name = data.Username;
User = data;
Body = "";
}
public void Add(Message msg)
{
Dialog.Add(msg);
Body += $"{msg.From}: {msg.Body} \n\n";
}
}
}
The binding doesn't work at all. I also want it to be one-way.
Text="{Binding CurrentPerson.Body}"
Not sure why the binding path contains CurrentPerson, when it should be CurrentDialog, but even that isn't supposed to be there. Since the DataContext is already set to CurrentDialog, you can simply bind the text to :
Text="{Binding Body}"
You need to implement INotifyPropertyChanged, so the WPF know when the property changed:
[Serializable][DataContract]
public class MessageDialog : INotifyPropertyChanged
{
#region public string Body
private string m_Body;
public string Body
{
get { return m_Body; }
private set
{
if (m_Body == value)
return;
m_Body = value;
this.NotifyPropertyChanged();
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}

Binding a nested object to a DataGrid in c# WPF

I am trying to bind the data displayed in a DataGrid to a dynamic list of object (WhisperModel) which is inside another object(WhisperReader). The DataGrid only displays the headers, but no values. How can I make the DataGrid dynamically update itself when the list "whispers" is changed?
Main Window XAML:
<DataGrid x:Name="whisperDataGrid" Margin="10,69,10,10" IsReadOnly="True" ItemsSource="{Binding}"/>
Main Window C#
public partial class MainWindow : Window
{
private WhisperReader wr;
public MainWindow()
{
InitializeComponent();
wr = new WhisperReader();
whisperDataGrid.DataContext = wr.whispers;
}
WhisperReader:
class WhisperReader
{
public ObservableCollection<WhisperModel> whispers { get; private set; }
public WhisperReader()
{
whispers = new ObservableCollection<WhisperModel>();
}
WhisperModel:
class WhisperModel
{
public DateTime sentTime { get; set; }
public string sender { get; set; }
public string message { get; set; }
}
I think your problem is that it doesn't know when to update itself because:
You have made the whispers list the data context.
The properties that you are binding to don't use INotifyPropertyChanged.
WhisperReader and WhisperModel are not public
All bindings must be public, must be properties, and must call the PropertyChanged method.
The PropertyChanged function triggers the binding updates.
Try this...
public partial class MainWindow : Window
{
private WhisperReader wr;
public MainWindow()
{
InitializeComponent();
wr = new WhisperReader();
whisperDataGrid.DataContext = wr;
}
public class WhisperReader : INotifyPropertyChanged
{
ObservableCollection<WhisperModel> _whispers;
public ObservableCollection<WhisperModel> whispers
{
get { return _whispers; }
private set
{
_whispers = value;
NotifyPropertyChanged();
}
}
public WhisperReader()
{
whispers = new ObservableCollection<WhisperModel>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class WhisperModel : INotifyPropertyChanged
{
public DateTime sentTime { get; set; }
private string _sender;
public string sender
{
get { return _sender; }
set { _sender = value; NotifyPropertyChanged();
}
private string _message;
public string message
{
get { return _message; }
set { _message = value; NotifyPropertyChanged();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
<DataGrid x:Name="whisperDataGrid" Margin="10,69,10,10" IsReadOnly="True" AutoGenerateColumns="True" ItemsSource="{Binding whispers}"/>

C# ListBox/ListView not showing elements - Metro UI

I am using a ListBox in my Metro UI media player, but the ListBox doesn't show any text when I run it. I don't have any errors or so, but there is no text. I have also tried to use a ListView and not a ListBox, but the result was the same. What can I do ?
I am using the next code for the ListBox on the XAML part of the page
<ListBox x:Name="ItemListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding PathToFile}"
FontSize="24" Margin="5,0,0,0" TextWrapping="Wrap" />
<TextBlock Text="{Binding HasVideo}"
FontSize="16" Margin="15,0,0,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
and the next code on the page C# code :
public static ListBox ListBoxIstance = null;
public MainPage()
{
InitializeComponent();
ListBoxIstance = ItemListBox;
ItemListBox.ItemsSource = Data_Repository.MediaData.MediaList;
ItemListBox.DataContext = Data_Repository.MediaData.MediaList;
}
where MediaList is a list declared as it follows
public static List<MediaFile> MediaList = new List<MediaFile>();
and MediaFile is a class
public class MediaFile
{
public TimeSpan Duration = TimeSpan.Zero;
public bool HasAudio = false;
public bool HasVideo = false;
public String PathToFile = null;
public MediaFile(string _pathToFile)
{
PathToFile = _pathToFile;
}
}
I am using the next code to update the MediaList and the ItemListBox
foreach (var pathToFile in files)
{
MediaList.Add(new MediaFile(pathToFile.Path));
MainPage.ListBoxIstance.UpdateLayout();
}
EDIT : After some suggestions :
That's how the MediaFile class looks like :
public class MediaFile : INotifyPropertyChanged
{
private bool _hasAudio;
public bool HasAudio
{
get { return _hasAudio; }
set
{
_hasAudio = value;
OnPropertyChanged("HasAudio");
}
}
private bool _hasVideo;
public bool HasVideo
{
get { return _hasVideo; }
set
{
_hasVideo = value;
OnPropertyChanged("HasVideo");
}
}
private String _pathToFile;
public String PathToFile
{
get { return _pathToFile; }
set
{
_pathToFile = value;
OnPropertyChanged("PathToFile");
}
}
public MediaFile(string pathToFile)
{
PathToFile = pathToFile;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
and I am using
public static ObservableCollection<MediaFile> MediaList = new ObservableCollection<MediaFile>();
You can't bind to public fields in your item class. Turn the class members into public properties:
public class MediaFile
{
public TimeSpan Duration { get; set; }
public bool HasAudio { get; set; }
public bool HasVideo { get; set; }
public String PathToFile { get; set; }
public MediaFile(string _pathToFile)
{
PathToFile = _pathToFile;
}
}
Then use an ObservableCollection instead of a List. That would automatically update the ItemsSource binding when items are added or removed. No need to call UpdateLayout.
public ObservableCollection<MediaFile> MediaList =
new ObservableCollection<MediaFile>();
If you also want to update the UI when any of the property values changes after a MediaFile object has been added to the list, you also have to implement the INotifyPropertyChanged interface:
public class MediaFile : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private bool hasVideo
public bool HasVideo
{
get { return hasVideo; }
set
{
hasVideo = value;
OnPropertyChanged("HasVideo");
}
}
// other properties
}
You can only bind with properties and not with fields. Change fields to properties.
public bool HasVideo {get;set;}
public String PathToFile {get;set;}

Categories

Resources