I get a simple DTO entity A loaded into my upshot viewmodel which is happily viewable via Knockoutjs.
My DTO A contains a List entities. So I can foreach over the elements inside A.
again:
class A
{
someprop;
List<B> childB;
}
Class B
{
somepropB;
}
So far so good. I can iterated over the data with no problem.
But if I change "someprop" inside an instance of A and SaveAll the server will not respond at all.
The updateData controlle method is not even invoked.
If I clear the childB.Clear() before transmitting it to the client, all is fine.
It seems the upshot is not able to update entities with collections?
There is a bit of work to do if you want such a scenario to work. Upshot only turns the parent entities in observable items. So only the javascript representation of class A is a knockout observable, the javascript representation of class B is not. Therefore Upshot is not aware of any changes in associated objects.
The solution is to map the entities manually. To make my life easier, I've used code from my 'DeliveryTracker' sample application in the code snippets below. In my blog article you can see an example of manual mapping: http://bartjolling.blogspot.com/2012/04/building-single-page-apps-with-aspnet.html so my examples below are working on the 'delivery' and 'customer' objects.
The server-side domain model
namespace StackOverflow.q9888839.UploadRelatedEntities.Models
{
public class Customer
{
[Key]
public int CustomerId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public virtual ICollection<Delivery> Deliveries { get; set; }
}
public class Delivery
{
[Key]
public int DeliveryId { get; set; }
public string Description { get; set; }
public bool IsDelivered { get; set; }
[IgnoreDataMember] //needed to break cyclic reference
public virtual Customer Customer { get; set; }
public virtual int CustomerId { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Delivery> Deliveries { get; set; }
}
}
The data service controller
The data service controller exposes the data conforming to OData standards on
"http://localhost:[yourport]/api/dataservice/GetCustomers". In order to be able to update both customers and deliveries you need to define an UpdateCustomer AND UpdateDelivery function
namespace StackOverflow.q9888839.UploadRelatedEntities.Controllers
{
public class DataServiceController : DbDataController<AppDbContext>
{
//Service interface for Customer
public IQueryable<Customer> GetCustomers()
{
return DbContext.Customers.Include("Deliveries").OrderBy(x => x.CustomerId);
}
public void InsertCustomer(Customer customer) { InsertEntity(customer); }
public void UpdateCustomer(Customer customer) { UpdateEntity(customer); }
public void DeleteCustomer(Customer customer) { DeleteEntity(customer); }
//Service interface for Deliveries
public void InsertDelivery(Delivery delivery) { InsertEntity(delivery); }
public void UpdateDelivery(Delivery delivery) { UpdateEntity(delivery); }
public void DeleteDelivery(Delivery delivery) { DeleteEntity(delivery); }
}
}
Client-side domain model
Add a new javascript file containing your client-side model. Here I'm explicitly turning every property into an knockout observable. The key for solving your problem is the line inside the constructor of the Customer object where I'm mapping the incoming deliveries into an observable array
/// <reference path="_references.js" />
(function (window, undefined) {
var deliveryTracker = window["deliveryTracker"] = {}; //clear namespace
deliveryTracker.DeliveriesViewModel = function () {
// Private
var self = this;
self.dataSource = upshot.dataSources.Customers;
self.dataSource.refresh();
self.customers = self.dataSource.getEntities();
};
deliveryTracker.Customer = function (data) {
var self = this;
self.CustomerId = ko.observable(data.CustomerId);
self.Name = ko.observable(data.Name);
self.Address = ko.observable(data.Address);
self.Latitude = ko.observable(data.Latitude);
self.Longitude = ko.observable(data.Longitude);
self.Deliveries = ko.observableArray(ko.utils.arrayMap(data.Deliveries, function (item) {
return new deliveryTracker.Delivery(item);
}));
upshot.addEntityProperties(self, "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models");
};
deliveryTracker.Delivery = function (data) {
var self = this;
self.DeliveryId = ko.observable(data.DeliveryId);
self.CustomerId = ko.observable(data.CustomerId);
self.Customer = ko.observable(data.Customer ? new deliveryTracker.Customer(data.Customer) : null);
self.Description = ko.observable(data.Description);
self.IsDelivered = ko.observable(data.IsDelivered);
upshot.addEntityProperties(self, "Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models");
};
//Expose deliveryTracker to global
window["deliveryTracker"] = deliveryTracker;
})(window);
The View
In the index.cshtml you initialize Upshot, specify custom client mapping and bind the viewmodel
#(Html.UpshotContext(bufferChanges: false)
.DataSource<StackOverflow.q9888839.UploadRelatedEntities.Controllers.DataServiceController>(x => x.GetCustomers())
.ClientMapping<StackOverflow.q9888839.UploadRelatedEntities.Models.Customer>("deliveryTracker.Customer")
.ClientMapping<StackOverflow.q9888839.UploadRelatedEntities.Models.Delivery>("deliveryTracker.Delivery")
)
<script type="text/javascript">
$(function () {
var model = new deliveryTracker.DeliveriesViewModel();
ko.applyBindings(model);
});
</script>
<section>
<h3>Customers</h3>
<ol data-bind="foreach: customers">
<input data-bind="value: Name" />
<ol data-bind="foreach: Deliveries">
<li>
<input type="checkbox" data-bind="checked: IsDelivered" >
<span data-bind="text: Description" />
</input>
</li>
</ol>
</ol>
</section>
The Results
When navigating to the index page, the list of customers and related deliveries will be loaded asynchronously. All the deliveries are grouped by customer and are pre-fixed with a checkbox that is bound to the 'IsDelivered' property of a delivery. The customer's name is editable too since it's bound to an INPUT element
I don't have enough reputation to post a screenshot so you will have to do without one
When checking or unchecking the IsDelivered checkbox now, Upshot will detect the change and post it to the DataService Controller
[{"Id":"0",
"Operation":2,
"Entity":{
"__type":"Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models",
"CustomerId":1,
"DeliveryId":1,
"Description":"NanoCircuit Analyzer",
"IsDelivered":true
},
"OriginalEntity":{
"__type":"Delivery:#StackOverflow.q9888839.UploadRelatedEntities.Models",
"CustomerId":1,
"DeliveryId":1,
"Description":"NanoCircuit Analyzer",
"IsDelivered":false
}
}]
When modifying the customer's name, Upshot will submit the changes when the input box loses focus
[{
"Id": "0",
"Operation": 2,
"Entity": {
"__type": "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models",
"Address": "Address 2",
"CustomerId": 2,
"Latitude": 51.229248,
"Longitude": 4.404831,
"Name": "Richie Rich"
},
"OriginalEntity": {
"__type": "Customer:#StackOverflow.q9888839.UploadRelatedEntities.Models",
"Address": "Address 2",
"CustomerId": 2,
"Latitude": 51.229248,
"Longitude": 4.404831,
"Name": "Rich Feynmann"
}
}]
So if you follow above procedure, Upshot will both update parent and child entities for you.
Related
The method which is used is ItemsProvider. Here are the 2 components.
Column.razor
#typeparam TTT
#code {
[CascadingParameter] public Grid<TTT>? MyGrid { get; set; }
protected override void OnInitialized()
{
MyGrid?.Add(this);
}
}
Grid.razor
#attribute [CascadingTypeParameter(nameof(TTT))]
#typeparam TTT
<CascadingValue Value="this">
#Columns
</CascadingValue>
#code {
[Parameter] public List<TTT>? Items { get; set; }
[Parameter] public RenderFragment? Columns { get; set; }
[Parameter] public Func<Task<IEnumerable<TTT>>>? ItemsProvider { get; set; }
private readonly List<Column<TTT>> MyColumns = new();
public void Add(Column<TTT> column)
{
MyColumns.Add(column);
}
}
When using these 2 components on the Index page below, I got the following error messages:
RZ10001 The type of component 'Column' cannot be inferred based on the values provided.
Consider specifying the type arguments directly using the following attributes: 'TTT'.
CS0411 The type arguments for method 'TypeInference.CreateColumn_1(RenderTreeBuilder, int)'
cannot be inferred from the usage. Try specifying the type arguments explicitly.
<Grid ItemsProvider="#Provide">
<Columns>
<Column></Column>
</Columns>
</Grid>
#code {
List<Test> tests = new();
async Task<IEnumerable<Test>> Provide()
{
await Task.Delay(1);
return new List<Test>() { new Test() { Name = "Name" } };
}
}
The problem can be fixed by:
A) Define the type explicitly for the Column:
<Column TTT="Test">
B) Define the Items parameter for the Grid:
<Grid ItemsProvider="#Provide" Items="tests">
C) Use the ItemsProvider delegate defined for the official Virtualize component.
Is there another solution that avoids A, B and C?
PS: Column.razor and Grid.razor are in a library.
I want to use radio group in blazor so after implementing edit form and select one of the radio button I got this error :
Microsoft.AspNetCore.Components.Forms.InputRadioGroup`1[EGameCafe.SPA.Models.GameModel] does not support the type 'EGameCafe.SPA.Models.GameModel'.
here is my edit form :
<EditForm Model="ViewModel" OnValidSubmit="HandleCreateGroup">
#if (ViewModel.Games.List.Any())
{
<InputRadioGroup Name="GameSelect" #bind-Value="Gamemodelsample">
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game" />
#game.GameName
<br />
}
</InputRadioGroup>
}
</EditForm>
#code{
public GameModel GameModelSample { get; set; } = new();
}
and GameModel is :
public class GameModel
{
public string GameId { get; set; }
public string GameName { get; set; }
}
The InputRadioGroup, like other Blazor components, supports only a limited amount of types like String or Int32. You had the right idea, but unfortunately, you run into a kind of limitation of Blazor.
You could try to create a wrapper field.
private String _selectedGameId = "<Your Default Id>";
public String SelectedGameId
{
get => _selectedGameId;
set
{
_selectedGameId = value;
// Set the property of the ViewModel used in your Model Property of the EditContext or any other property/field
ViewModel.SelectedGame = ViewModel.Games.List?.FirstOrDefault(x => x.GameId == value);
}
}
Use the property SelectedGameId as the bind value of the InputRadioGroup component.
<InputRadioGroup Name="GameSelect" #bind-Value="SelectedGameId" >
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game.GameId" />
#game.GameName
<br />
}
</InputRadioGroup>
As an alternative, you can create a custom component that inheriting from InputRadioGroup to create a kind of GameBasedInputRadioGroup. If you are interested I can post a sample.
Because in your code #bind-Value="Gamemodelsample",you are trying to bind GameName(string) to Gamemodelsaple(object), which will cause type mismatch problems.
You only need to modify your code to:
#bind-Value="GameModelSample.GameName"
I'm trying to write this controller that accepts a POST request.
I need this controller to add a new book, and also add that new books bookId to another object called a StoreList.
So I am trying to pass in the new bookList, and the storeList that needs the bookId added to it.
// POST: api/BookList
[HttpPost]
public async Task<ActionResult<BookList>> PostBookList(BookList bookList, StoreList storeList)
{
_context.BookList.Add(bookList);
await _context.SaveChangesAsync();
_context.Entry(storeList).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!StoreListExists(storeId))
{
return NotFound();
}
else
{
throw;
}
}
return CreatedAtAction("GetBookList", new { id = bookList.BookId }, bookList);
}
Here is my API endpoint:
https://localhost:44362/api/BookList/
And these are the two objects I'm passing in the BODY of the request (the new bookList and the existing storeList):
{
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7",
"bookTitle": "Is this a test 2?"
},
{
"storeId": "0001f801-6909-4b6e-8652-e1b49745280f",
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7"
}
But whenever I try to 'hit' that endpoint, I get this error:
System.InvalidOperationException HResult=0x80131509 Message=Action
'DocumentStorageAPI.Controllers.Book.BookListController.PostBookList
(DocumentStorageAPI)' has more than one parameter that was
specified or inferred as bound from request body. Only one
parameter per action may be bound from body. Inspect the following
parameters, and use 'FromQueryAttribute' to specify bound from query,
'FromRouteAttribute' to specify bound from route, and
'FromBodyAttribute' for parameters to be bound from body: BookList
bookList StoreList storeList
How can I get my controller to allow me to add a new bookList and update the needed storeList?
Thanks!
The body of the request should be just one object and the PostBookList method must have only one parameter (with the [FromBody] attribute). If you need both classes to use within the method create a new class like this:
public class PostBookListRequest
{
public BookList BookList { get; set; }
public StoreList StoreList { get; set; }
}
change the PostBookList method to
public async Task<ActionResult<BookList>> PostBookList([FromBody]PostBookListRequest request)
{
// Your logic here
}
And in the BODY of the request do
{
"bookList": {
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7",
"bookTitle": "Is this a test 2?"
},
"storeList": {
"storeId": "0001f801-6909-4b6e-8652-e1b49745280f",
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7"
}
}
You can use body like this:
{
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7",
"title": "Is this a test 2?",
"storeId" "0001f801-6909-4b6e-8652-e1b49745280f"
}
In Controller you need additional class BookListBinding with this 3 fields, which you will use for create you 2 objects, for example.
[HttpPost]
public async Task<ActionResult> PostBookList(BookListBinding binding)
{
var bookList = new BookList
{
Id = binding.BookId,
Title = binding.Title
});
var storeList = new StoreList
{
Id = binding.StoreId,
BookId = binding.BookId
}
// you work with _context
return CreatedAtAction("GetBookList", new { id = binding.BookId }, bookList);
}
Why you need change StoreList? What is it?
You have to create a new model like so-
public class AddBookToStoreModel
{
public Book BookToAdd { get; set; }
public StoreList BookStoreList { get; set; }
}
You have to add the same to the controller definition, like this-
[HttpPost]
public async Task<ActionResult<BookStore>> PostBookList(AddBookToStoreModel model)
Also you need to create the individual Models for Book and StoreList like below-
public class Book
{
public Guid BookId { get; set; }
public string BookTitle { get; set; }
}
public class StoreList
{
public Guid StoreId { get; set; }
public Guid BookId { get; set; }
}
The Json that you post to the controller will look like below-
{
"BookToAdd": {
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7",
"bookTitle": "Is this a test 2?"
},
"BookStoreList": {
"storeId": "0001f801-6909-4b6e-8652-e1b49745280f",
"bookId": "bc381612-c63b-4438-b35b-161a3a568fc7"
}
}
I hope this works for you.
I have a template rendering another template via foreach: viewModel.foos. On this template I would like to do something like this: #Html.RouteLink("View", "Foo", new { id = fooId, text = fooName }). Being fooId and fooName properties of the view model.
I've added this to my template:
<a data-bind="attr: { href: Url }">View</a>
And this to my Foo object:
public class Foo : FooBase {
public long FooId { get; set; }
public string FooName { get; set; }
public string Url {
get {
return string.Format("/foo/{0}/{1}", FooId, FooName
}
}
}
The cons:
Painfully scalable.
The pros:
Simplicity.
This does not answer question, but I will keep it here because it shows how you can do it for a more SPA like approuch.
If you are looking to move navigation to client check out Durandal or my light weight SPA engine
https://github.com/AndersMalmgren/Knockout.Bootstrap.Demo
Old answer before question was updated
You have to do this from your model (Which is a good thing because it should not be done in the view)
Let your subview model take the parameters as constructor arguments
MyViewModel = function() {
this.subView = ko.observable();
};
MyViewModel.prototype = {
loadSubView: function() {
this.subView(new SubViewModel(this.params));
}
};
edit: Tip test this lib for templating
http://jsfiddle.net/hwTup/
I'm a fluent nhibernate newbie and I'm struggling mapping a hierarchy of polymorhophic objects. I've produced the following Model that recreates the essence of what I'm doing in my real application.
I have a ProductList and several specialised type of products;
public class MyProductList
{
public virtual int Id { get; set; }
public virtual string Name {get;set;}
public virtual IList<Product> Products { get; set; }
public MyProductList()
{
Products = new List<Product>();
}
}
public class Product
{
public virtual int Id { get; set; }
public virtual string ProductDescription {get;set;}
}
public class SizedProduct : Product
{
public virtual decimal Size {get;set;}
}
public class BundleProduct : Product
{
public virtual Product BundleItem1 {get;set;}
public virtual Product BundleItem2 {get;set;}
}
Note that I have a specialised type of Product called BundleProduct that has two products attached.
I can add any of the specialised types of product to MyProductList and a bundle Product can be made up of any of the specialised types of product too.
Here is the fluent nhibernate mapping that I'm using;
public class MyListMap : ClassMap<MyList>
{
public MyListMap()
{
Id(ml => ml.Id);
Map(ml => ml.Name);
HasManyToMany(ml => ml.Products).Cascade.All();
}
}
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(prod => prod.Id);
Map(prod => prod.ProductDescription);
}
}
public class SizedProductMap : SubclassMap<SizedProduct>
{
public SizedProductMap()
{
Map(sp => sp.Size);
}
}
public class BundleProductMap : SubclassMap<BundleProduct>
{
public BundleProductMap()
{
References(bp => bp.BundleItem1).Cascade.All();
References(bp => bp.BundleItem2).Cascade.All();
}
}
I haven't configured have any reverse mappings, so a product doesn't know which Lists it belongs to or which bundles it is part of.
Next I add some products to my list;
MyList ml = new MyList() { Name = "Example" };
ml.Products.Add(new Product() { ProductDescription = "PSU" });
ml.Products.Add(new SizedProduct() { ProductDescription = "Extension Cable", Size = 2.0M });
ml.Products.Add(new BundleProduct()
{
ProductDescription = "Fan & Cable",
BundleItem1 = new Product() { ProductDescription = "Fan Power Cable" },
BundleItem2 = new SizedProduct() { ProductDescription = "80mm Fan", Size = 80M }
});
When I persist my list to the database and reload it, the list itself contains the items I expect ie MyList[0] has a type of Product, MyList[1] has a type of SizedProduct, and MyList[2] has a type of BundleProduct - great!
If I navigate to the BundleProduct, I'm not able to see the types of Product attached to the BundleItem1 or BundleItem2 instead they are always proxies to the Product - in this example BundleItem2 should be a SizedProduct.
Is there anything I can do to resove this either in my model or the mapping?
Thanks in advance for your help.
As it stands, the BundleItem1 and BundleItem2 properties will always have a Product proxy because NH creates your proxies without touching the database, so it doesn't know if they are Products or some derived type. But when you call a method on your bundle items, NH should hit the DB and load the correct record, and you should get polymorphic behavior.
You could test this out. Add an override of ToString to your SizedProduct:
public override string ToString()
{
return "I'm a sized product!";
}
Then load your BundleProduct and do this:
Debug.WriteLine(bp.BundleItem1.ToString());
Debug.WriteLine(bp.BundleItem2.ToString());
You should find that the second call prints out "I'm a sized product!", and this will demonstrate that you have working polymorphism.
Assuming this all worked as I've described, its time to tackle the real question: what exactly do you want to do? Maybe you could provide some code that doesn't actually work as you would like it to.