-
Notifications
You must be signed in to change notification settings - Fork 2
04 Calculated property
In this workshop, you will update the Customers window in order to have something like this:
If you do not use WAQS, you have many problem to solve at this point.
- Where do you want to define the customer total spent, the order total and the order detail amount
- In View Model? It won't be easy
- In Model? It means that your business logic will be split between layers which is not the best thing for maintainability.
- We already specifies the formula to calculate the customer total spent in the customers list query. Will you duplicate it? It won't be easy to reuse it.
- We have to deal with the PropertyChanged pattern which duplicates (one more time) the formula logic. Indeed, if we want to change the way to calculate an order total, we will also have to change the PropertyChanged logic.
So it means that we will lose a lot of time to code something that won't as good as expected for maintainability and reliability (you increase the risk of error) points of view.
Keep calm and let WAQS solve this for you!
Alike Intentional Programming idea, WAQS allows you to define your specification outside your application code.
Instead of any marketing talk, the best would probably be to allow you to test it yourself and make your own opinion. It's what you will start in this workshop.
When we generated the code in the server, WAQS created a folder Specifications.
This folder is the default location for your business code.
In this folder, add 4 new classes:
ProductSpecifications
public static class ProductSpecifications
{
public static string GetFullName(this Product product)
{
return product.Name + " (" + product.Category.Name + ")";
}
}
OrderDetailSpecifications
public static class OrderDetailSpecifications
{
public static double GetAmount(this OrderDetail orderDetail)
{
return orderDetail.UnitPrice * orderDetail.Quantity * (1 - orderDetail.Discount);
}
public static string GetProductFullName(this OrderDetail orderDetail)
{
return orderDetail.Product.GetFullName();
}
}
OrderSpecifications
public static class OrderSpecifications
{
public static double GetTotal(this Order order)
{
return order.OrderDetails.Sum(od => od.GetAmount());
}
}
CustomerSpecifications
public static class CustomerSpecifications
{
public static double GetTotalSpent(this Customer customer)
{
return customer.Orders.Sum(o => o.GetTotal());
}
public static string GetFullName(this Customer customer)
{
return customer.CompanyName + " " + customer.ContactName;
}
}
This business code won't be executed. It is just a model to write your business code.
WAQS will analyze this code to generate the code you need in the different layers.
In order to use calculated properties, you need to use an extension method with a name starting with Get and only one parameter. (we will see later that this last assertion is not completely true).
Now, update solution generated code as we did in the previous workspace.
Then open MainWindowViewModel and replace this query
Customers = await (from c in _context.Customers.AsAsyncQueryable()
let totalSpent = c.Orders.Sum(o => o.OrderDetails.Sum(od => od.Quantity * od.UnitPrice * (1 - od.Discount)))
orderby totalSpent descending
select new CustomerInfo
{
Id = c.Id,
Name = c.CompanyName + " " + c.ContactName,
TotalSpent = (double?)c.Orders.Sum(o => o.OrderDetails.Sum(od => od.Quantity * od.UnitPrice * (1 - od.Discount))) ?? 0
}).ExecuteAsync();
By this one
Customers = await (from c in _context.Customers.AsAsyncQueryable()
let totalSpent = c.TotalSpent
orderby totalSpent descending
select new CustomerInfo
{
Id = c.Id,
Name = c.FullName,
TotalSpent = totalSpent
}).ExecuteAsync();
As you can see, from the specifications methods GetTotalSpent and GetFullName, WAQS generated 2 properties TotalSpent and FullName that are usable in LINQ To WAQS.
If you remember the workshop 01, there is an explanation of the (double?)totalSpent ?? 0 usage.
With WAQS you don't need to think about this anymore.
Indeed, analyzing the specifications method, WAQS can see that the result type is not nullable and the expression uses a join so it adds the ?? 0 itself.
Now update the CustomerWindow.xaml with this xaml.
Then, in the CustomerViewModel, add the following code
private Order _selectedOrder;
public Order SelectedOrder
{
get { return _selectedOrder; }
set
{
_selectedOrder = value;
NotifyPropertyChanged.RaisePropertyChanged(nameof(SelectedOrder));
}
}
Rename the LoadCustomerAsync method to LoadAsync.
In this method, we need to include the customer order and their details
Customer = await _context.Customers.AsAsyncQueryable().FirstOrDefault(c => c.Id == customerId).IncludeOrdersWithExpression(orders => orders.IncludeOrderDetails()).ExecuteAsync();
Then, to be able to select a product in the order details grid, we need to load the products and to show the product full name, we need the categories.
We can easily do it using this code
private async Task LoadAsync(string customerId)
{
Customer = await _context.Customers.AsAsyncQueryable().FirstOrDefault(c => c.Id == customerId).IncludeOrdersWithExpression(orders => orders.IncludeOrderDetails()).ExecuteAsync();
await _context.Products.AsAsyncQueryable().IncludeCategory().ExecuteAsync();
}
public IClientEntitySet<INorthwindClientContext, Product> Products
{
get { return _context.Products; }
}
But it will be bad to load the whole products entities and the whole categories ones.
BTW, categories contains an image and we just need the name.
Product FullName is read-only in the current context.
WAQS provides some methods to calculate this value in the SQL query and to get the value from the DB to the client via the server.
So in order to avoid categories loading, you can use this
await _context.Products.AsAsyncQueryable().WithFullName().ExecuteAsync();
But even like this, in this sample, you are loading many useless Product properties.
So you should prefer using this query
await _context.Products.AsAsyncQueryable().Select(p => new Product { Id = p.Id, FullName = p.FullName }).ExecuteAsync();
Now, you can play with it and see that if you change a value, all calculation are automatically updated.
WAQS analyzing the specifications method determines the dependencies of calculated properties. Then it generates all the PropertyChanged logic.
If you want to update your specifications, you just have to run the Update Solution Generated Code command.
Now you are almost done for this workshop but you have a bug.
If you remove an order detail, you will have an exception on saving changes.
The reason is the fact that you bind SelectedOrder.OrderDetails as ItemsSource of the DataGrid.
So when you remove an OrderDetail from the DataGrid, it removes the OrderDetail from the Order OrderDetails collection but it does not remove the OrderDetail from the WAQS client context.
So when you save it, WAQS tries to save an update on the OrderDetail with OrderId to 0 which is not allowed by the Database.
So when you remove an OrderDetail from the DataGrid you have to remove it from the context.
You can do this updating the SelectedOrder property
public Order SelectedOrder
{
get { return _selectedOrder; }
set
{
if (_selectedOrder != null)
{
_selectedOrder.OrderDetails.CollectionChanged -= OrderDetailsCollectionChanged;
}
_selectedOrder = value;
NotifyPropertyChanged.RaisePropertyChanged(nameof(SelectedOrder));
if (_selectedOrder != null)
{
_selectedOrder.OrderDetails.CollectionChanged += OrderDetailsCollectionChanged;
}
}
}
private void OrderDetailsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var od in e.OldItems.Cast<OrderDetail>())
{
_context.OrderDetails.Remove(od);
}
}
}
As always, you can get the final source code here.