I'm working with Catel, MVVM, WPF and am wondering about how to work with nested/hiearchical data.
Let's say from a database I've got a list of Customers, each with a list of Invoices, each with a list of InvoiceItems. Customers own many Invoices which own many InvoiceItems.
I've got a working solution, but I do not like it. My approach was to build a collection of classes that would act a kind of like an ado.net “dataset.” A class would represent each layer of the hiearchy.
This top level class, CustomerModel, would contain a collection of of InvoiceBlocks:
CustomerModel
ObservableCollection of < InvoicesBlocks >
Each InvoceBlock would contain an Invoice and a collection of InvoiceItems:
InvoiceBlock
Invoice
ObservableCollection of < InvoiceItems >
It seemed clever until wading through the databinding path= satements. There are also times when I have to loop through the sets mamaully to update totals, defeating a major selling point of MVVM.
So, I've decided to learn more about grouping with LINQ queries and databinding. Is this the way the pros do it?
What you can do is make each view model responsible for using the right services to retrieve the data.
Note that I did not use Catel properties to make it easy to understand, but you can simply use Catel.Fody or rewrite the properties to get Catel properties.
public class CustomerViewModel
{
private readonly IInvoiceService _invoiceService;
public CustomerViewModel(ICustomer customer, IInvoiceService invoiceService)
{
Argument.IsNotNull(() => customer);
Argument.IsNotNull(() => invoiceService);
Customer = customer;
_invoiceService = invoiceService;
}
public ICustomer Customer { get; private set; }
public ObservableCollection<IInvoice> Invoices { get; private set; }
protected override void Initialize()
{
var customerInvoices = _invoiceService.GetInvoicesForCustomer(Customer.Id);
Invoices = new ObservableCollection<IInvoice>(customerInvoices);
}
}
public class InvoiceViewModel
{
private readonly IInvoiceService _invoiceService;
public InvoiceViewModel(IIinvoice invoice, IInvoiceService invoiceService)
{
Argument.IsNotNull(() => invoice);
Argument.IsNotNull(() => invoiceService);
Invoice = invoice;
_invoiceService = invoiceService;
}
public IInvoice Invoice { get; private set; }
public ObservableCollection<IInvoiceBlock> InvoiceBlocks { get; private set; }
protected override void Initialize()
{
var invoiceBlocks = _invoiceService.GetInvoiceBlocksForInvoice(Invoice.Id);
InvoiceBlocks = new ObservableCollection<IInvoiceBlock>(invoiceBlocks);
}
}
Now you are fully in control what happens when.
Related
I feel like I'm missing something obvious here; I'm using .Net 5 with Entity Framework Core. The problem is that the foreign key is correct, but the associated navigation property is always empty and has no data. Do I have to do something with the fluent framework, or do something special with my includes?
I have 3 simplified entities and a database context method in this example, the project is much too large to include entirely. In the method, CalendarEvents is a DbSet:
public class CalendarEvent: IJsonSerializable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// Gets the personnel associated with this event
/// </summary>
public virtual List<SchedulePerson> SchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class SchedulePerson : IJsonSerializable, ICloneable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int EmployeeId { get; set; }
public virtual Employee Employee { get; set; }
public virtual CalendarEvent AssociatedCalendarEvent { get; set; }
}
public class Employee : IJsonSerializable
{
[Key]
public int Id { get; set; }
public virtual List<SchedulePerson> AssociatedSchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class DbContext : DbContext
{
public DbSet<CalendarEvent> CalendarEvents { get; set; }
public DbSet<SchedulePerson> SchedulePeople { get; set; }
public DbSet<Employee> Employees { get; set; }
public DbContext(DbContextOptions<VibrationContext> options): base(options)
{
}
public CalendarEvent GetEvent(int calendarEventId)
{
var currentCalEvent = this.CalendarEvents.Where(x => x.Id == calendarEventId);
var dummy1 = currentCalEvent.FirstOrDefault();
var dummy2 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ToList();
var dummy3 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ThenInclude(people => people.Employee).ToList();
return currentCalEvent.FirstOrDefault();
}
}
In this case Employee is the associated navigation property, and the associated key EmployeeId, is correct. For the moment I've added extra logic that will populate each employee per schedule person separately, manually based on the foreign key EmployeeId, when I get the event, but I'd rather not have to add logic like that each time. If that's something unavoidable then that's fine, but I'd like to do things properly and let Entity Framework Core handle as much as possible.
For additional context:
SchedulePerson -> CalendarEvent is a many-to-one relationship
Employee -> SchedulePerson is a many-to-one relationship
In other words a calendar event can contain many schedule persons, but a schedule person can only be associated with one calendar event.
Employee can be associated with many schedule persons, but each schedule person is only associated with one employee.
There should only be one employee for each real person, but there can be multiple SchedulePersons for each real person.
Thank you for your help and let me know if there is any more information I can provide.
Also if anything else looks bad or wrong in these code snippets please let me know.
Edit, this is what I have to do if I want to get the employees in my request:
private void UpdateEmployeeContents(CalendarEvent calendarEvent)
{
foreach (SchedulePerson person in calendarEvent.SchedulePeople)
{
person.Employee = this.Employees.Where(x => x.Id == person.EmployeeId).FirstOrDefault();
}
}
I have created ChatMessageGroup and ChatMessageGroupCollection and a ListView with ItemsSource set to CollectionViewSource:
<ListView x:Name="ChatMessageLv" ItemsSource="{Binding SelectedChat.ChatMessageGroupCollection.Cvs.View}" ItemTemplateSelector="{StaticResource ChatMessageDataTemplateSelector}">
public class ChatMessageGroup : IGrouping<DateTime, ChatMessage>, INotifyCollectionChanged
{
private ObservableCollection<ChatMessage> _chatMessages;
public DateTime Key { get; set; }
public ObservableCollection<ChatMessage> ChatMessages
{
get { return _chatMessages; }
set
{
if (_chatMessages != null)
_chatMessages.CollectionChanged -= CollectionChanged;
_chatMessages = value;
_chatMessages.CollectionChanged += CollectionChanged;
}
}
public ChatMessageGroup()
{
ChatMessages = new ObservableCollection<ChatMessage>();
}
public IEnumerator<ChatMessage> GetEnumerator()
{
return ChatMessages.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
public class ChatMessageGroupCollection : IEnumerable<ChatMessageGroup>
{
private readonly ObservableCollection<ChatMessageGroup> _groups;
public ObservableCollection<ChatMessage> Source { get; set; }
public CollectionViewSource Cvs { get; set; }
public ChatMessageGroupCollection(ObservableCollection<ChatMessage> messages)
{
Source = messages;
messages.CollectionChanged += Messages_CollectionChanged;
var groups = messages
.GroupBy(GetGroupKey)
.Select(x => new ChatMessageGroup()
{
Key = x.Key,
ChatMessages = x.OrderBy(GetGroupKey).ToObservableOrDefault()
})
.OrderBy(x => x.Key);
_groups = new ObservableCollection<ChatMessageGroup>(groups);
Cvs = new CollectionViewSource() { IsSourceGrouped = true, Source = _groups };
}
...
Everything works fine in here except the changes inside group collection:
_groups.Add(new ChatMessageGroup()); -> this line reflect changes in ListView
but if I do like this: _groups[0].ChatMessages.Add(new ChatMessage()) it doesn't work even though ChatMessageGroup is implementing INotifyCollectionChanged and is raised every time ChatMessages ObservableCollection is changed.
The workaround is to update ChatMessages and remove group from _groups and then add it again but it's not a solution. Refresh() on CollectionViewSource is not available in UWP. Are there any other solutions?
Whilst this doesn't technically qualify as an answer to the question I do think it might qualify as an architectural answer.
The way to make a grouped list view update based on the underlying collection changing in WPF is as simple as setting IsLiveGroupingRequested to true on the CollectionViewSource instance declared in XAML.
As I've been working my way through UWP (after nearly a decade of WPF) I've come to the conclusion that Microsoft are suggesting through omission that this isn't the right approach to the problem for UWP. So going to the lengths of implementing the feature yourself could be construed as missing the point somewhat.
In my particular situation I've decided to change my approach entirely and implement the rendering as multiple instance of ListViews as opposed to forcing an old paradigm onto a new platform.
The result of this actually made me arrive at a solution that improved my UX to boot.
Food for thought ...
I found a lot of explanations about this issue, but nothing that really helped me. The thing is simple. I have two tables on my dataModel: Events and TimeStamps, both have the field EntryID, which is the relation between them(the tables are in fact Views, I can't perform changes on DB, I can only query them).On my domainService, I have the created methods for getting data from each of the tables. So far, I am able to fill a dataGrid with data from only one of the tables, but what I really need is to display from both tables. In T-SQL it would be something like:
Select e.EntryID,t.closed_time
from Events e inner join TimeStamps t
on e.EntryID=t.EntryID
So I want to display on my dataGrid the Entry_ID and closed_time.I appreciate your help for solving my problem
I tried a new custom class
public class CustomTable
{
public string EntryId { get; set; }
public int closed_time { get; set; }
}
public IQueryable<CustomTable> GetJoined()
{
return (from i in this.ObjectContext.Events
join p in this.ObjectContext.TimeStamps p
on i.Entry_ID equals p.Entry_ID
select new CustomTable
{
EntryId = i.Entry_ID,
closed_Time = p.Closed_TIME
});
}
This is the additional code I added by myself, I'm pretty sure something is missing, this method and the class itself were added on my service.cs
This is the final code and procedures done, do not forget to build the project after each step:
1- Opened a new class under Myproject.Web(Add-->new item-->class)
namespace Myproject.Web
{
public class CustomTable
{
[Key]
public string EntryId { get; set; }
public int closed_Time { get; set; }
}
}
2-Added on IncidentService.cs:
public IQueryable<CustomTable> GetJoined()
{
return (from i in this.ObjectContext.Events
join p in this.ObjectContext.TimeStamps p
on i.Entry_ID equals p.Entry_ID
select new CustomTable
{
EntryId = i.Entry_ID,
closed_Time = p.Closed_TIME
});
}
3-Added on Mypage.xaml.cs
public MyPage()
{
InitializeComponent();
this.dataGrid1.ItemsSource = _IncidentContext.CustomTables;
_IncidentContext.Load(_IncidentContext.GetJoinedQuery());
DataGridTextColumn entry = new DataGridTextColumn();
entry.Binding = new System.Windows.Data.Binding("EntryId");
entry.Header = "Entry Id";
DataGridTextColumn closed = new DataGridTextColumn();
closed.Binding = new System.Windows.Data.Binding("closed_Time");
closed.Header = "Closed Time";
dataGrid1.Columns.Add(entry);
dataGrid1.Columns.Add(closed);
}
I hope this will help others with same issue, I spent 3 days working on this solution!!
I've recently started using the MVVM pattern in silverlight, and i'm not sure if i am using it correctly.
GUI
I currently have a MainView that has combobox of stock market sectors. When the user selects a sector (eg ENERGY) and clicks the Add button a list of stocks for that sector are displayed in a listbox. By the side of each stock in the listbox is a remove button that allows you to remove the individual stock from the listbox.
I have implemented the following ViewModels. (Below is just an indication of the code)
public class MainViewModel
{
public SectorViewModel CurrentSector
{
get;
set;
}
public string SelectedSector
{
get;
set;
}
public void AddSectorClickedCommand()
{
CurrentSector = new SectorViewModel(SelectedSector);
}
}
public class SectorViewModel
{
public ObservableCollection<StockViewModel> Stocks = new ObservableCollection<StockViewModel>();
public SectorViewModel(string sector)
{
List<Stocks> stocklist = StockProvider.GetStocks(sector);
for each (var s in stocklist)
{
StockViewModel svm = new StockViewModel(s);
svm.Remove+= { //Remove svm from Stocks collection logic
Stocks.add(svm);
}
}
}
My question is; in whcih viewmodel is it best to add the code implementation for the Remove button of each row in the listbox?? The Remove button should remove the StockViewModel from the SectorViewModel.Stocks collection.
I have currently added the RemoveClicked method to the StockViewModel(as shown above). This code fires an event back to the SectorViewModel and the RemoveStock method of the SectorViewModel removes the StockViewModel from the Stock collection.
Is there a better way to implement this remove functionality? I'm new to MVVM and am not sure if this is the best approach to develop this functionility, since the SectorViewModel needs to register to events of a StockViewModel.
Personally I don't like events because you should unsubscribe from them and also they can be used where it isn't appropriate.
I would use the constructor parameter to handle the remove command, something like this:
public class StockViewModel
{
public StockViewModel(Stock stock, Action<StockViewModel> removeCommandAction)
{
//...
this.RemoveCommand = new DelegateCommand(() => removeCommandAction(this));
}
}
public class SectorViewModel
{
public SectorViewModel()
{
//...
StockViewModel svm = new StockViewModel(s, this.RemoveStock);
Stocks.add(svm);
}
private void RemoveStock(StockViewModel stock)
{
//...
}
}
Another approach is to use some kind of the EventAggregator pattern, for example, the Messenger class from the MVVM light Toolkit. But I think that it is an overkill for such simple task:
public StockViewModel(Stock stock, IMessenger messenger)
{
//...
this.RemoveCommand = new DelegateCommand(() =>
messenger.Send(new NotificationMessage<StockViewModel>(this, RemoveItemNotification)));
}
public SectorViewModel(IMessenger messenger)
{
//...
messenger.Register<NotificationMessage<StockViewModel>>(this, msg =>
{
if (msg.Notification == StockViewModel.RemoveItemNotification)
{
this.RemoveStock(msg.Content);
}
}
}
Also I heard that Silverlight 5 supports binding to a relative source.
So there is the 3rd approach. I'm not sure whether this example works, but at least it should:
<Button Content="Remove"
Command="{Binding DataContext.RemoveCommand RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" />
public class SectorViewModel
{
public SectorViewModel()
{
this.RemoveCommand = new DelegateCommand(obj => this.RemoveStock((StockViewModel)obj));
}
public ICommand RemoveCommand { get; set; }
}
The last example is the most preferred by the way and is used in WPF applications because WPF has always had RelativeSource binding.
I have a association in my poco class, ex:
public class Category() {
[Key]
public int id { get; set}
public string Name { get; set; }
/* HERE */
public virtual ICollection<Book> Books {get; set;}
}
public class Book() {
[Key]
public int id { get; set}
public string Name { get; set; }
}
I use MVVM patern, MVVM Light and RIA Services Toolkit. My Domain Service implementation contains a method GetCategories that include their books, ex:
public IQueriable<Category> GetCategories()
{
return Model.Categories.Include("Books").OrderBy(pCategory => pCategory.Name);
}
In my ViewModel I have a DomainCollectionView that load GetGruposQuery. I also have a property for bind a grid and other controls, like:
public ICollectionView CollectionViewCategories {
get { return myDomainCollectionViewCategories;}
}
I need get a child property CollectionView.Books for bind my controls and ADD, REMOVE itens in view, but this property is only EntityCollection and isn't a DomainCollectionView that contains methods for ADD, REMOVE, etc.
How I can get the current Books property (of CollectionViewCategories) as DomainCollectionView in my ViewModel?
Thank you!
I solve this question with: (CollectionViewCategories.CurrentItem as Category).Books.Remove(CollectionViewBooks.CurrentItem as Book)
private ICollectionView CreateView(Object source)
{
CollectionViewSource cvs = new CollectionViewSource();
cvs.Source = source;
return cvs.View;
}
//...
//After CollectionViewCategories loaded:
CollectionViewCategories.CurrentChanged += (s, e) =>
{
if (CollectionViewCategories.CurrentItem != null)
{
CollectionViewBooks = CreateView(fContext.Categories.Where(p => p.Id == (CollectionViewCategories.CurrentItem as Category).Id).FirstOrDefault().Books);
}
else
{
CollectionViewBooks = null;
}
RaisePropertyChanged("CollectionViewBooks");
};
Why do you need it ? EntityCollection do have these methods (Add,Remove...)
If you obtain an error while invoking them, it could lead to the fact that the Book class is not fully exposed to the client (lack of insert/update/delete methods on the domainservice)
Just to clarify, DomainCollectionView is inteded to wrap an EntityCollection primarily for better dealing with MVVM and binding in general (see this link)