I have a View that contains a ListView in which I load an ObservableCollection.
After that I am going to load some external data from our MySQL Database foreach item in my collection.
I would like to perform these in an async Task to prevent ui freezing.
The updating works fine for all items that are not visible in the ListView. Because they are virtualized?
The items that are currently on the ListView visible are not beeing updated. How can I notify the View to Update the Properties of the items?
Note: the Viewmodel implements InotifyPropertiChanged via a BaseViewModel
public class ProductViewModel : BaseTypes.BaseViewModel
{
public ProductViewModel()
{
}
public void GetProducts(string filter)
{
if (filter.Equals(""))
{
return;
}
Products.Clear();
Product product = new Product();
product.GetProductsByHersteller(filter);
if (product.productsList != null)
{
product.productsList.ToList().ForEach(Products.Add);
}
GetRanking();
}
private ObservableCollection<Product> _Products = new ObservableCollection<Product>();
public ObservableCollection<Product> Products { get { return _Products; } private set { _Products = value; } }
async Task GetRanking()
{
await Task.Run(() => GetRank());
}
private void GetRank()
{
foreach (Product item in Products)
{
item.Rank.Get(item.Asin);
NotifyPropertyChanged(nameof(Products));
}
}
}
Property in the Product class
private Produkt.Ranking rank = new Produkt.Ranking();
public Produkt.Ranking Rank { get => rank; set => rank = value; }
Solution:
Added INotifyPropertyChanged to the Ranking Class.
Code in the ViewModel now is this:
public void GetProducts(string filter)
{
if (filter.Equals(""))
{
return;
}
Products.Clear();
Product product = new Product();
product.GetProductsByHersteller(filter);
if (product.productsList != null)
{
product.productsList.ToList().ForEach(Products.Add);
}
GetRanking();
}
private ObservableCollection<Product> _Products = new ObservableCollection<Product>();
public ObservableCollection<Product> Products { get { return _Products; } private set { _Products = value; } }
async void GetRanking()
{
await Task.Run(() => GetRank());
}
private void GetRank()
{
foreach (Product item in Products)
{
item.Rank.Get(item.Asin);
}
}
Your Product class should implement INotifyPropertyChanged.
Then no need to call NotifyPropertyChanged in your GetRank() method.
Related
I have a WPF application in which I have to two datagrids. The first one shows Albums and the second one corresponding Songs, based on the selection in the first datagrid. I can also add an Album, which is then saved to the database. Now I want to select an album in the first datagrid and add a song to that album in the second datagrid. All songs have an AlbumID as a foreign key in my database. I don't know how to handle this parameter AlbumID when adding a Song to the database, my query crashes as expected.
I would like to know how I can tell the SongViewModel to add the Song with the AlbumID given by the SelectedAlbum.
Query to add Album:
public AlbumData AddAlbumEntry(AlbumData albumData)
{
album albumEntry = new album
{
AlbumName = albumData.AlbumName,
Year = albumData.AlbumYear,
};
_context.albums.Add(albumEntry);
_context.SaveChanges();
return new AlbumData
{
AlbumID = albumEntry.AlbumID,
AlbumYear = albumEntry.Year,
AlbumName = albumEntry.AlbumName,
};
}
Query to add song:
public SongData AddSongEntry(SongData songData)
{
song songEntry = new song
{
SongName = songData.SongName,
SongNumber = songData.SongNumber,
};
_context.songs.Add(songEntry);
_context.SaveChanges();
return new SongData
{
SongID = songEntry.SongID,
SongNumber = songEntry.SongNumber,
SongName = songEntry.SongName,
};
}
AlbumViewModel to connect to View:
public AlbumData AddAlbumEntry(AlbumData albumData)
{
var controller = new BandManagerController();
return controller.AddAlbumEntry(albumData);
}
public void AlbumToDatabase(AlbumData data)
{
AddAlbumEntry(data);
ExecuteCancelCommand();
}
SongViewModel to connect to view:
public SongData AddSongEntry(SongData songData)
{
var controller = new BandManagerController();
return controller.AddSongEntry(songData);
}
public void SongToDatabase(SongData data)
{
AddSongEntry(data);
ExecuteCancelCommand();
}
I also have a SelectedAlbum property to fill the SongLists:
public AlbumViewModel SelectedAlbum
{
get
{
return _selectedAlbum;
}
set
{
if (_selectedAlbum != value)
{
_selectedAlbum = value;
}
NotifyPropertyChanged("SelectedAlbum");
}
}
I would try by creating View model that is easy to bind to ui. View model should have SelectedAlbum and SelectedSong properties so when you create new song you will already have information in view model about selected album, then you can set album id on song model.
class ViewModel : BindableBase
{
private IDatabase _database;
private Album _album;
private Song _song;
public ObservableCollection<Album> Albums { get; private set; }
public ObservableCollection<Song> AlbumSongs { get; private set; }
public ICommand AddAlbumCommand {get; private set;}
public ICommand AddSongCommand {get; private set;}
public Album SelectedAlbum
{
get => _album;
set { LoadAlbumSongs(album); SetProperty(ref _album, value); }
}
public Song SelectedSong
{
get => _song;
set => SetProperty(ref _song, value);
}
public ViewModel(IDatabase database)
{
_database = database;
Albums = new ObservableCollection<Album>();
AlbumSongs = new ObservableCollection<Song>();
AddAlbumCommand = new RelayCommand (() => AddNewAlbum(), () => true);
AddSongCommand = new RelayCommand (() => AddNewSong(), () => SelectedAlbum != null);
}
public void AddNewAlbum()
{
SelectedAlbum = new Album();
}
public void AddNewSong()
{
SelectedSong = new Song();
SelectedSong.AlbumID = SelectedAlbum.AlbumID;
}
public void SaveAlbum()
{
if (SelectedAlbum.ID == 0) {
Albums.Add(SelectedAlbum);
}
_database.SaveAlbum(SelectedAlbum);
}
public void SaveSong()
{
if (SelectedSong.ID == 0) {
AlbumSongs.Add(SelectedSong);
}
_database.SaveSong(SelectedSong);
}
}
I found a solution. In the AlbumListViewModel I open the dialog where I can add Songs. I bound the AlbumId from the SongModel to the AlbumID from my AlbumModel via Selected Album. Now I can select an album and add a song with the corresponding AlbumID. The method for the command to add a song with the "add" button looks like:
private void AddSong()
{
addSong.ShowDialog(new SongViewModel(new SongData { AlbumID = SelectedAlbum.AlbumID }));
}
I'm writing a wpf application, where I have music albums and corresponding songs. I can add albums and corresponding songs. But now I want to to refresh the view when a change to the database is made. I found many possible solutions, but as I'm new to wpf and c# I don't know which one would suite my code.
In my MainView have an album list and a add button which opens another window where I can add data with a textbox.
AlbumListViewModel
#region Constants
IWindowManager addAlbum = new WindowManager();
IWindowManager addSong = new WindowManager();
private AlbumViewModel _selectedAlbum;
private SongViewModel _selectedSong;
#endregion
#region Constructor
public AlbumListViewModel()
{
Albums = new ObservableCollection<AlbumViewModel>(GetAlbumList());
AddAlbumCommand = new RelayCommand(x => AddAlbum());
AddSongCommand = new RelayCommand(x => AddSong());
}
#endregion
#region Properties
public ICommand AddAlbumCommand { get; private set; }
public ICommand AddSongCommand { get; private set; }
public ObservableCollection<AlbumViewModel> Albums { get; set; }
public AlbumViewModel SelectedAlbum
{
get
{
return _selectedAlbum;
}
set
{
if (_selectedAlbum != value)
{
_selectedAlbum = value;
}
NotifyPropertyChanged("SelectedAlbum");
}
}
public SongViewModel SelectedSong
{
get
{
return _selectedSong;
}
set
{
if (_selectedSong != value)
{
_selectedSong = value;
}
NotifyPropertyChanged("SelectedSong");
}
}
#endregion
#region Methods
public List<AlbumViewModel> GetAlbumList()
{
var controller = new BandManagerController();
return controller.GetAlbumList()
.Select(a => new AlbumViewModel(a))
.ToList();
}
private void AddAlbum()
{
addAlbum.ShowDialog(new AlbumViewModel(new AlbumData()));
}
private void AddSong()
{
addSong.ShowDialog(new SongViewModel(new SongData { AlbumID = SelectedAlbum.AlbumID }));
}
It opens the AlbumView where I add albums to the database.
public class AlbumViewModel : Screen
{
#region Constants
private AlbumData _data;
#endregion
#region Constructor
public AlbumViewModel(AlbumData data)
{
_data = data;
SongListVM = new SongListViewModel(data.AlbumID);
SaveAlbumToDatabase = new RelayCommand(x => AlbumToDatabase(data));
}
#endregion
#region Properties
public SongListViewModel SongListVM { get; set; }
public ICommand SaveAlbumToDatabase { get; private set; }
public string AlbumName
{
get
{
return _data.AlbumName;
}
set
{
if (_data.AlbumName != value)
{
_data.AlbumName = value;
NotifyOfPropertyChange("AlbumName");
}
}
}
public int AlbumID
{
get
{
return _data.AlbumID;
}
set
{
if (_data.AlbumID != value)
{
_data.AlbumID = value;
NotifyOfPropertyChange("AlbumID");
}
}
}
public string AlbumYear
{
get
{
return _data.AlbumYear;
}
set
{
if (_data.AlbumYear != value)
{
_data.AlbumYear = value;
NotifyOfPropertyChange("AlbumYear");
}
}
}
#endregion
#region Methods
public AlbumData AddAlbumEntry(AlbumData albumData)
{
var controller = new BandManagerController();
return controller.AddAlbumEntry(albumData);
}
public void ExecuteCancelCommand()
{
(GetView() as Window).Close();
}
public void AlbumToDatabase(AlbumData data)
{
AddAlbumEntry(data);
ExecuteCancelCommand();
}
#endregion
}
The AddAlbumEntry Method in the ALbumView is in a different class which is the connections to my database. I already use an ObservableCollection but don't know how to tell it the Database was updated.
Thanks in advance!
Just want to answer my question. I just changed my AddAlbum method to use a Deactivated event, to reload the Collection after the Dialog closes like:
private void AddAlbum()
{
var vm = new AlbumViewModel(new AlbumData());
vm.Deactivated += (s, e) => GetAlbumList();
addAlbum.ShowDialog(vm);
}
so i have this list of products.
now, what i want to do is loop over this list in another class with a foreach.
how do i make a reference to AllProducts in the class i want to loop in?
public List<Product> AllProducts;
AllProducts = new List<Product>();
foreach (product p in AllProducts)
{
Console.WriteLine(p);
}
public class A
{
//Property
//must be set somewhere (e.g. constructor)
public List<Product> Products { get; private set; }
//Method
public List<Product> GetProducts()
{
//return list of products
}
}
public class B
{
public void Display()
{
var a = new A();
foreach (var product in a.Products /*OR a.GetProducts()*/)
{
Console.WriteLine(product);
}
}
}
It'll be better if You create a method in class that have that list and name it something like "showProducts()" and iterate with Console.WriteLine and in Your main class call that method.
public List<Product> fillProduct()
{
List<Product> AllProducts = new List<Product>();
//fill
}
private void btnAdd_Click(object sender, EventArgs e)
{
foreach (product p in fillProduct())
{
Console.WriteLine(p);
}
}
You can do like this
I have a model collection that is referenced from a most parts of my code.
public class TraceEntryQueue
{
private readonly Queue<TraceEntry> _logEntries;
public TraceEntryQueue()
{
_logEntries = new Queue<TraceEntry>();
}
public void AddEntry(TraceEntry newEntry)
{
_logEntries.Enqueue(newEntry);
}
public List<TraceEntry> GetAsList()
{
return _logEntries.ToList();
}
}
This collection is presented in one of my views.
public class LoggingViewModel : ViewModelBase
{
private ICollectionView _customers;
public ICollectionView Customers
{
get
{
return _customers;
}
private set
{
_customers = value;
}
}
public LoggingViewModel(TraceEntryQueue traceQueue)
{
Customers = CollectionViewSource.GetDefaultView(traceQueue.GetAsList());
Customers.SortDescriptions.Add(new SortDescription("Index", ListSortDirection.Descending));
Customers.Refresh();
}
}
The question is how I can notify my view to reload the collection when I add new entries via
public void AddEntry(TraceEntry newEntry)
{
_logEntries.Enqueue(newEntry);
}
Use an observable collection instead of a queue. This will automatically notify your view when the collection is updated (add/remove etc.)
public class TraceEntryQueue
{
private readonly ObservableCollection<TraceEntry> _logEntries;
public TraceEntryQueue()
{
_logEntries = new ObservableCollection<TraceEntry>();
}
public void AddEntry(TraceEntry newEntry)
{
_logEntries.Add(newEntry);
}
public ObservableCollection<TraceEntry> GetLogEntries()
{
return _logEntries;
}
}
public class TraceEntryQueue
{
private readonly Queue<TraceEntry> _logEntries;
private readonly ICollectionView _logEntriesView;
public TraceEntryQueue()
{
_logEntries = new Queue<TraceEntry>();
_logEntriesView = CollectionViewSource.GetDefaultView(logEntries.ToList());
}
//....
public void AddEntry(TraceEntry newEntry)
{
_logEntries.Enqueue(newEntry);
_logEntriesView.SourceCollection = logEntries.ToList();
_logEntriesView.Refresh();
}
public ICollectionView GetAsView()
{
return _logEntriesView;
}
}
Use GetAsView and bind directly to your datagrid / listbox / listview.
As #RobJohnson mentionned, you can use an ObservableCollection. However, if you need to create your own collection class, you should implement the INotifyCollectionChanged interface:
public class TraceEntryQueue : INotifyCollectionChanged
{
private readonly Queue<TraceEntry> _logEntries;
public TraceEntryQueue()
{
_logEntries = new Queue<TraceEntry>();
}
public void AddEntry(TraceEntry newEntry)
{
_logEntries.Enqueue(newEntry);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newEntry));
}
public List<TraceEntry> GetAsList()
{
return _logEntries.ToList();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
CollectionChanged(this, e);
}
}
}
Thanks to some of the advice I have got previously on Stack Overflow I have been making good progress in my understanding of MVVM. However, it is when things start to get more complicated that I am still struggling.
I have the view below which is for the purpose of entering orders. It is bound to a DataContext of OrderScreenViewModel.
<StackPanel>
<ComboBox Height="25" Width="100" DisplayMemberPath="CustomerCode" SelectedItem="{Binding Path=Order.Customer}" ItemsSource="{Binding Path=Customers}"></ComboBox>
<ComboBox Height="25" Width="100" DisplayMemberPath="ProductCode" SelectedItem="{Binding Path=CurrentLine.Product}" ItemsSource="{Binding Path=Products}"></ComboBox>
</StackPanel>
The first combobox is used to select a Customer. The second combobox is used to select a ProductCode for a new OrderLine.
There are the items that I cannot work out how to achieve in MVVM:
1) When a Customer is selected update the Products combobox so that its item source only shows Products that have the same CustomerId as the CustomerDto record selected in the combobox
2) When Load is called set the SelectedItem in the Customers combobox so that it displays the Customer with the CustomerId equal to the one on the OrderDto.
3) Apply, the same process as 1) so that only Products belonging to that Customer are shown / loaded and set the SelectedItem on the Products combobox so that it is pointing to the entry with the same ProductId as is contained on the OrderLineDto
I am not sure how to proceed or even if I have got the responsibilities of my viewmodels correct. Maybe it has something to do with NotifyPropertyChanged? Any pointers on how I can achieve the above will be greatly appreciated. I am sure if I get this right it will help me greatly in my app. Many thanks Alex.
public class OrderScreenViewModel
{
public WMSOrderViewModel Order { get; private set; }
public WMSOrderLineViewModel CurrentLine { get; private set; }
public OrderScreenViewModel()
{
Order = new WMSOrderViewModel();
CurrentLine = new WMSOrderLineViewModel(new OrderLineDto());
}
public void Load(int orderId)
{
var orderDto = new OrderDto { CustomerId = 1, Lines = new List<OrderLineDto> { new OrderLineDto{ProductId = 1 }} };
Order = new WMSOrderViewModel(orderDto);
}
public List<CustomerDto> Customers
{
get{
return new List<CustomerDto> {
new CustomerDto{CustomerId=1,CustomerCode="Apple"},
new CustomerDto{CustomerId=1,CustomerCode="Microsoft"},
};
}
}
public List<ProductDto> Products
{
get
{
return new List<ProductDto> {
new ProductDto{CustomerId=1,ProductId=1,ProductCode="P100",Description="Pepsi"},
new ProductDto{CustomerId=1,ProductId=2,ProductCode="P110",Description="Coke"},
new ProductDto{CustomerId=2,ProductId=3,ProductCode="P120",Description="Fanta"},
new ProductDto{CustomerId=2,ProductId=4,ProductCode="P130",Description="Sprite"}
};
}
}
public class WMSOrderLineViewModel
{
private ProductDto _product;
private OrderLineDto _orderLineDto;
public WMSOrderLineViewModel(OrderLineDto orderLineDto)
{
_orderLineDto = orderLineDto;
}
public ProductDto Product { get { return _product; }
set{_product = value; RaisePropertyChanged("Product"); }
}
public class WMSOrderViewModel
{
private ObservableCollection<WMSOrderLineViewModel> _lines;
private OrderDto _orderDto;
public ObservableCollection<WMSOrderLineViewModel> Lines { get { return _lines; } }
private CustomerDto _customer;
public CustomerDto Customer { get{return _customer;} set{_customer =value; RaisePropertyChanged("Customer") }
public WMSOrderViewModel(OrderDto orderDto)
{
_orderDto = orderDto;
_lines = new ObservableCollection<WMSOrderLineViewModel>();
foreach(var lineDto in orderDto.Lines)
{
_lines.Add(new WMSOrderLineViewModel(lineDto));
}
}
public WMSOrderViewModel()
{
_lines = new ObservableCollection<WMSOrderLineViewModel>();
}
}
You need to make Products and Customers type ObservableCollection.
When you change these observablecollections in your viewmodel they will update the view, because OC's already implement INotifyPropertyChanged.
Order and CurrentLine should just be a type and not really be called a ViewModel.
1) You're going to have to do this when the setter is called on the SelectedItem of the Customer combobox is selected.
2) You'll need to do this probably in the ctr of the OrderScreenViewModel by using your logic to determine what Customer to change the CurrentLine.Customer too. If you do this in the ctr, this will set the value before the binding takes place.
3) Again, as long as you make changes to the ObservableCollection the combobox is bound to, it will update the UI. If you make a change to what the SelectedItem is bound to just make sure you call RaisedPropertyChanged event.
ETA: Change the xaml to this, bind to SelectedProduct and SelectedCustomer for the SelectedItem properties
<StackPanel>
<ComboBox Height="25" Width="100" DisplayMemberPath="CustomerCode" SelectedItem="{Binding Path=SelectedCustomer}" ItemsSource="{Binding Path=Customers}"></ComboBox>
<ComboBox Height="25" Width="100" DisplayMemberPath="ProductCode" SelectedItem="{Binding Path=SelectedProduct}" ItemsSource="{Binding Path=Products}"></ComboBox>
</StackPanel>
this should get you started in the right direction, everything, all logic for building customers and products by the customer id needs to happen in your repositories.
public class OrderScreenViewModel : INotifyPropertyChanged
{
private readonly IProductRepository _productRepository;
private readonly ICustomerRepository _customerRepository;
public OrderScreenViewModel(IProductRepository productRepository,
ICustomerRepository customerRepository)
{
_productRepository = productRepository;
_customerRepository = customerRepository;
BuildCustomersCollection();
}
private void BuildCustomersCollection()
{
var customers = _customerRepository.GetAll();
foreach (var customer in customers)
_customers.Add(customer);
}
private ObservableCollection<Customer> _customers = new ObservableCollection<Customer>();
public ObservableCollection<Customer> Customers
{
get { return _customers; }
private set { _customers = value; }
}
private ObservableCollection<Product> _products = new ObservableCollection<Product>();
public ObservableCollection<Product> Products
{
get { return _products; }
private set { _products = value; }
}
private Customer _selectedCustomer;
public Customer SelectedCustomer
{
get { return _selectedCustomer; }
set
{
_selectedCustomer = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer"));
BuildProductsCollectionByCustomer();
}
}
private Product _selectedProduct;
public Product SelectedProduct
{
get { return _selectedProduct; }
set
{
_selectedProduct = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
DoSomethingWhenSelectedPropertyIsSet();
}
}
private void DoSomethingWhenSelectedPropertyIsSet()
{
// elided
}
private void BuildProductsCollectionByCustomer()
{
var productsForCustomer = _productRepository.GetById(_selectedCustomer.Id);
foreach (var product in Products)
{
_products.Add(product);
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
public interface ICustomerRepository : IRepository<Customer>
{
}
public class Customer
{
public int Id { get; set; }
}
public interface IProductRepository : IRepository<Product>
{
}
public class Product
{
}
Here's what the standard IRepository looks like, this is called the Repository Pattern:
public interface IRepository<T>
{
IEnumerable<T> GetAll();
T GetById(int id);
void Save(T saveThis);
void Delete(T deleteThis);
}