zkoss MVVM changes to model force grid to reload - data-binding

I am using ZKOSS MVVM.
So in View I am using a Listbox and it's bound (#load) to a list model object in ViewModel.
What I understand from documentation, if I change the model
1: Add an object to list model from View Model at index 0
I should see the latest object be appended at top of the Listbox.
2: Remove an item from model
I should see that particular row from Listbox be removed.
Note: It's an interface like social network e.g. Facebook wall when someone create a post and new post is appended to the posts list. If a post is deleted only that post is deleted from the list
Well, it does happen (new item gets appended/deleted item gets removed) but the whole Listbox reloads and not just that particular row which was added or removed.
Why is that? Why Listbox reloads fully on list model change.
Any idea?
Here are the code snippets (Use Case: Add new post is applicable. On creating new post whole Listbox reloads every time):
View
<z:div style="height: 100%; padding: 0px; margin: 0px;" apply="org.zkoss.bind.BindComposer"
viewModel="#id('want_vm') #init('want.WantDesktopVM')">
<z:div zclass="content">
<g:render template="../css/list/noEffectList"></g:render>
<z:div hflex="1" width="100%" visible="#load(want_vm.toggleInput)" style="margin-bottom: 5px; padding: 5px">
<z:vbox>
<z:textbox id="postInput" multiline="true" value="" width="690px" height="50px"/>
<z:div hflex="1" width="100%" style="text-align: right; padding-right: 5px">
<z:button label="Post" zclass="button rect theme" onClick="#command('post', text=postInput.value)"/>
</z:div>
</z:vbox>
</z:div>
<z:listbox model="#load(want_vm.posts)" emptyMessage="No new posts found." style="border:none;">
<z:template name="model" var="iwant">
<listitem style="margin-top: 10px">
<listcell>
<hbox hflex="true">
<div zclass="dpFrame small">
<image height="50px" width="50px" content="#load(iwant.from) #converter('converter.UserActorDisplayPicConverter')" />
</div>
<vbox hflex="true" zclass="post">
<hbox hflex="true">
<label value="#load(iwant.from) #converter('converter.ActorDisplayNameConverter')" zclass="displayName"/>
</hbox>
<hbox hflex="true">
<label value="#load(iwant.textData)" zclass="post_data" multiline="true" maxlength="25"/>
</hbox>
<hbox>
<label value="#load(iwant.dateCreated) #converter('converter.SinceDateConverter')" zclass="since"/>
</hbox>
</vbox>
</hbox>
</listcell>
</listitem>
</z:template>
</z:listbox>
</z:div>
ViewModel
class WantDesktopVM {
UserActorManagerService userActorManagerService
ActivityManagerService activityManagerService
UserActor me
UserActor profile
String error = null
String view = 'iwant'
#Wire
Textbox postInput
private List<Activity> posts = []
#Init
public void init(#ContextParam(ContextType.COMPONENT) Component component,
#ContextParam(ContextType.VIEW) Component view) {
profile = Executions.current.getAttribute("profile")
me = Executions.current.getAttribute("me")
loadPosts()
}
#AfterCompose
public void afterCompose(#ContextParam(ContextType.VIEW) Component view) {
Selectors.wireComponents(view, this, false);
}
public boolean isMyProfile() {
return me.id == profile.id
}
public UserActor getMe() {
return this.me
}
public boolean isToggleInput() {
return this.view == 'iwant' && isMyProfile()
}
public List<Activity> getPosts() {
println "Getting posts ...${posts.size()}"
return this.posts
}
private List<Activity> loadPosts() {
if(view == 'iwant') {
posts = Activity.createCriteria().list() {
eq 'from', profile
eq 'type', ACTIVITY_TYPE.WANT
order("lastUpdated", "desc")
}
} else {
posts = ActorActivitySpace.createCriteria().list() {
projections {property("activity")}
eq 'actor', profile
activity {
ne 'from', profile
eq 'type', ACTIVITY_TYPE.WANT
}
order("lastUpdated", "desc")
}
}
return posts
}
#NotifyChange(['posts', 'toggleInput'])
#Command
public void render(#BindingParam('view') String view) {
println "Changing view ..."
this.view = view
loadPosts()
}
#NotifyChange('posts')
#Command
public void post(#BindingParam('text') String text) {
println "Posting text: $text"
postInput.setValue("")
if(text) {
Activity want = activityManagerService.want(me.id, text)
println"Want ID : $want.id"
posts.addAll(0, [want])
}
}
}

You use #NotifyChange('posts') to tell ZK that the whole list has changed. The grid doesn't try to examine the list, it simply replaces its current ListModel with the new list -> full reload.
If you don't want that, you will have to use the methods of the ListModel used by the grid to update the ui. That way, the grid will know exactly which rows have changed and only update those.
[EDIT] To achieve what you want, replace List<Activity> posts with ListModelList<Activity> posts = new ListModelList<Activity>()
When the activities change, you must update this list (i.e. call add() or addAll()) to update individual rows. You can no longer load everything from the database, you must merge changes in the database with the existing list.

Related

how do I bind a blazorise RichTextEdit component to a model property

I am attempting to use a blazorise RichTextEdit component within a form. I cannot seem to get the value to be set initially to the value of a provided model property.
<Form Model="#company">
<Validations #ref="validations" Mode="ValidationMode.Auto" ValidateOnLoad="false" Model="#model">
<Validation>
<Field>
<FieldLabel>Company Website</FieldLabel>
<TextEdit Role="TextRole.Url" #bind-Text="#model.Property1" Placeholder="Enter your website" Size="Size.Large">
<Feedback>
<ValidationError />
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>About</FieldLabel>
<RichTextEdit #ref="richTextEditRef"
ContentChanged="#OnContentChanged"
Theme="RichTextEditTheme.Snow"
PlaceHolder="Tell us about the company..."
SubmitOnEnter="false"
ToolbarPosition="Placement.Top">
<Editor></Editor>
<Toolbar>
<RichTextEditToolbarGroup>
<RichTextEditToolbarButton Action="RichTextEditAction.Bold" />
<RichTextEditToolbarButton Action="RichTextEditAction.Italic" />
<RichTextEditToolbarSelect Action="RichTextEditAction.Size">
<RichTextEditToolbarSelectItem Value="small" />
<RichTextEditToolbarSelectItem Selected="true" />
<RichTextEditToolbarSelectItem Value="large" />
<RichTextEditToolbarSelectItem Value="huge">Very Big</RichTextEditToolbarSelectItem>
</RichTextEditToolbarSelect>
<RichTextEditToolbarButton Action="RichTextEditAction.List" Value="ordered" />
<RichTextEditToolbarButton Action="RichTextEditAction.List" Value="bullet" />
</RichTextEditToolbarGroup>
<!-- Custom toolbar content -->
<RichTextEditToolbarGroup Float="Float.Right">
</RichTextEditToolbarGroup>
</Toolbar>
</RichTextEdit>
</Field>
</Validations>
<Button Color="Color.Success" Clicked="#Submit">Save</Button>
</Form>
#code {
private Model model { get; set; } = new Model();
private RichTextEdit richTextEditRef;
Validations validations;
protected override async Task OnInitializedAsync()
{
model = await modelService.GetByAccount();
//await richTextEditRef.SetHtmlAsync(model.Property2);
}
public async Task OnContentChanged()
{
model.Property2 = await richTextEditRef.GetHtmlAsync();
}
async void Submit()
{
Console.WriteLine("Form Submitted");
var result = await modelService.Post(model);
}
}
The modelService only returns a single record, which id does successfully. I am able to retrieve the input value using richTextEditRef.GetHtmlAsync() however I cannot find a way to use the
richTextEditRef.SetHtmlAsync(company.About) method to initially set the value of the RichTextEdit.
I have tried calling it after calling the modelService as seen in the commented code, but this is inconstant as it is often excecuted prior to the service returning the record. I have also attempted overriding the OnAfterRenderAsync method but I am not sure I am doing that correctly.
Too much wasted time on this, please help!?
Well after much trial and error I got this to work. Hopefully someone else will benefit from this:
in the editor component add:
<Editor>#((MarkupString)model.Property2)</Editor>
in the #code add a new property:
public string newRichTextValue { get; set; }
in the OnContentChanged() method set the new property:
newRichTextValue = await richTextEditRef.GetHtmlAsync();
in the Submit() method set the model.Property2 value to the new property:
model.Property2 = newRichTextValue;
If the data is stored as an HTML string. You can render the html inside the
<Editor/> tag with the help of (MarkupString) See MarkupString
<Editor>#((MarkupString)HtmlString)</Editor>
This will initialize the plugin with data correctly formatted in the editor.

Blazor: binding to a MultiSelectList (ideally with a checkbox)

Experimenting with Blazor (Server, if that makes any difference), and I'm having difficulty getting binding to a MultiSelectList to work....
Bit of background: I'm dealing with EF Core and have a Many-to-Many relationship, let's say between people and cars. I'm currently loading a page that shows the existing details, and allowing the user to update this page.
So in my Service, I load my Person entity from the DB, and this includes the details of all the cars they currently own. I also load the list of all the available cars. My Service method then creates a MultiSelectList and adds it to my ViewModel (to be returned to the Razor Page):
Service method
vm.CarSelector = new MultiSelectList(
allCars,
nameof(Car.CarId),
nameof(Car.Name),
person.OwnedCars.Select(oc => oc.CarId));
This is fictitious code, but I hope you get the picture. When debugging this (in the Service method) I can see that this MultiSelectList has an entry for every car, and the ones that are already selected are showing as Selected. Great!
Blazor Razor Page
So, this is where I come unstuck.... I can't work out how to do the two-way data-binding of a Razor control to this object.
I'm trying to use an <InputSelect />, but that might not be the best control to use.
ideally (actually, that's more of a "must have"), each option should have CheckBox.
I'm wondering whether the use of a MultiSelectList really buys me anything
Checkboxes are a bit different in blazor. Normally you would use the bind-value attribute on an input element as shown below, however, this is not recommended as you will only be able to read the value and NOT update the UI by changing the boolean value via code:
<input type="checkbox" #bind-value="#item.Selected"/>
Instead, use the #bind syntax for checkboxes, which is much more robust and will work both ways (changing the bound boolean value from code & interacting with the checkbox on the UI). See the syntax below:
<input type="checkbox" #bind="#item.Selected"/>
The bind attribute will automatically bind your boolean value to the "checked" property of the html element.
Also make sure you are binding to the "Selected" property rather than the "Value" property.
Using the built in bind will prevent the need to manually setup events as you did in your answer. You can also get rid of the if/else block and merge your code into a single code flow since you are now binding to the boolean rather than setting the checked property manually. If you still need to tap into an event to fire off some process(maybe hiding parts of UI on checking a box), I'd suggest using the onclick event and manually passing in the multiselect Item for each line. Here is the final code:
#foreach(var item in list)
{
<input type="checkbox" #bind="item.Selected" #onclick="(()=>handleClick(item))" />
}
#foreach(var item in list.Where(x=>x.Selected))
{
<p> Item #item.Text is Selected</p>
}
#code {
MultiSelectList list = new MultiSelectList(new List<Car> { new Car { Year = 2019, Make = "Honda", Model = "Accord" }, new Car { Make = "Honda", Model = "Civic", Year = 2019 } });
private void handleClick(SelectListItem item)
{
//Do something crazy
}
}
I got this to work with a component that takes the MultiSelectList as a parameter. There may be more elegant ways to achieve this (please do update if you know of a better way).
#using Microsoft.AspNetCore.Components
#using Microsoft.AspNetCore.Mvc.Rendering
<div class="multiselect">
<div id="checkboxes">
#foreach (var item in this.Items)
{
<div>
<label for="#item.Value">
#if (item.Selected)
{
<input type="checkbox" id="#item.Value" checked="checked" #onchange="#((e) => CheckboxChanged(e, item.Value))" />
}
else
{
<input type="checkbox" id="#item.Value" #onchange="#((e) => CheckboxChanged(e, item.Value))" />
}
#item.Text
</label>
</div>
}
</div>
</div>
#code
{
[Parameter]
public MultiSelectList Items { get; set; } = null!;
private void CheckboxChanged(ChangeEventArgs e, string key)
{
var i = this.Items.FirstOrDefault(i => i.Value == key);
if (i != null)
{
i.Selected = (bool)e.Value;
}
}
}

Acumatica NoteID in Grid doesn't save

I am adding a notes field to the grid of AP202000 (Vendor Prices) and when I enter something and hit save, it doesn't stick. I have added the NoteIndicator="True" to the grid on the AP202000 Page and I have added this code to the APVendorPrice DAC, not as an extension:
#region NoteID
public abstract class noteID : PX.Data.IBqlField
{
}
protected Guid? _NoteID;
[PXNote]
public virtual Guid? NoteID
{
get
{
return this._NoteID;
}
set
{
this._NoteID = value;
}
}
#endregion
Here is the code for the grid header:
<px:PXGrid ID="grid" runat="server" DataSourceID="ds" Height="144px" Style="z-index: 100" Width="100%" Caption="Sales Prices"
SkinID="Details" FilterShortCuts="True" NoteIndicator="True" FilesIndicator="True" ActivityIndicator="True" AdjustPageSize="Auto" AllowPaging="True" SyncPosition="true" >
I noticed other forums where it said if I would do these two things then the notes field would work, however this is not my case. This is for the note field to be on the grid with each row and would save to each individual row on the grid, not the notes field at the top.

MVC adding items to sessions and displaying in view

I'm working on a ASP.NET web shop but I'm stuck on a particular part. I'm trying to create a session whenever a user (not logged in) adds a CD or a DVD to their shopping cart. That way they can go to their cart (/shopping_cart) and see all of the products they have added.
However, I can't make it work. Whenever I click on 'add product' nothing really happens.
Here's my code:
ProductsController:
// POST: Producten/Details/9
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddToCart(int id)
{
Product p = db.Producten.Find(id);
if (p != null)
{
Models.Winkelkar kar;
if(Session["kar"] == null)
{
kar = new Models.Winkelkar();
}else
{
kar = (Models.Winkelkar)Session["winkelkar"];
}
kar.AddProduct(p);
Session["winkelkar"] = kar;
//var products = Session["kar"] as List<Product>;
}
return View();
}
My Models.Winkelkar looks like this:
public partial class Winkelkar
{
private List<Product> _List = new List<Product>();
public List<Product> GetList()
{
// geeft een lijst van pbjecten terug
return _List;
}
public void AddProduct(Product p)
{
_List.Add(p);
}
}
And my view form looks like this:
<form action="" method="post>
#Html.AntiForgeryToken()
<input type="hidden" name="id" value="#Model.ID" />
<input type="submit" name="toevoegen" value="Toevoegen" />
</form>
Now I'm trying to display the session on the same page, I'll move it to /shopping_cart once I know it works, but I'm trying to display it like this:
#Session["winkelkar"]
I've looked through many other solutions here, but nothing really seems to work.
EDIT
As pointed out below, I didn't have an action in my form. I changed it to:
<form action="#Url.Action("AddToCart", "Producten")" method="POST">
...
</form>
Getting one step closer.
You used the name "winkelkar" as your session key everywhere, expect on this line:
if(Session["kar"] == null)
That means you will never find your object, and always recreate a new one.
For this reason, you should avoid using a string literal for session keys, and declare a constant instead.
And as mason pointed out in his comment, your form has an empty action attribute. You should have something like that:
<form action="#Url.Action("AddToCart", "Products")" method="post">
....
</form>

Populate a viewmodel with a newly added database ID

This is a follow up to a question that was asked yesterday.
I have a viewmodel, which shows a list of objectives. Using jquery I can add a new objectives line to the screen (the ID is set to 0 for any new objectives listed). When I click on the Save button to Post the objective list back to the controller, the controller loops through the objective list, and checks the ID against the database. If the ID is NOT found, it creates a new objective, adds this to the DB context, and saves te changes. It then retreives the ID, and returns the View(model) to the View.
The problem is, although the ID in the model, is updated to the database ID - when the model is rendered in the View again, it's ID is still 0. So if I click Save again, it again, re-adds the "new objective added previously" to the database again.
My controller is shown below:
//
// POST: /Objective/Edit/model
[HttpPost]
public ActionResult Edit(ObjectivesEdit model)
{
if (model.Objectives != null)
{
foreach (var item in model.Objectives)
{
// find the database row
Objective objective = db.objectives.Find(item.ID);
if (objective != null) // if database row is found...
{
objective.objective = item.objective;
objective.score = item.score;
objective.possscore = item.possscore;
objective.comments = item.comments;
db.SaveChanges();
}
else // database row not found, so create a new objective
{
Objective obj = new Objective();
obj.comments=item.comments;
obj.objective = item.objective;
obj.possscore = item.possscore;
obj.score = item.score;
db.objectives.Add(obj);
db.SaveChanges();
// now get the newly created ID
item.ID = obj.ID;
}
}
}
return View(model);
}
My ID is being set in the controller:
EDIT: Another example here, showing model.Objectives1.ID being updated:
However when the view renders it, it reverts to 0:
The Objectives list is determined as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcObjectives2.Models
{
public class ObjectivesEdit
{
public IEnumerable<Objective> Objectives { set; get; }
public ObjectivesEdit()
{
if (Objectives == null)
Objectives = new List<Objective>();
}
}
}
The View has:
#model MvcObjectives2.Models.ObjectivesEdit
#using (Html.BeginForm())
{
#Html.EditorFor(x=>x.Objectives)
<button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> Save</button>
}
and in my EditorTemplate (objective.cshtml):
#model MvcObjectives2.Models.Objective
<div class="objec">
<div>
#Html.TextBoxFor(x => x.objective})
</div>
<div>
#Html.TextBoxFor(x => x.score})
</div>
<div>
#Html.TextBoxFor(x => x.possscore})
</div>
<div>
#Html.TextBoxFor(x => x.comments})
#Html.HiddenFor(x => x.ID) // This is the ID where it should now show the new ID from the database, but shows 0
</div>
</div>
I suspect the issue is somewhere in my controller - but I would appreciate any advise on how to get my View to render the new ID of the added objective.
After rewording my search, I came across several posts which say this is by design. A Posted form expects to display what it sent to the controller, if the same page is shown again.
However, you can add this, which will flush ModelState, and apparantly show the updated values from the model, updated in the controller:
ModelState.Clear();
return View(model);
I'm not certain if this has any other effect yet - but for now, it appears to work ok.
Thanks, Mark
The Html.HiddenFor has bitten me before in a similar scenario. The problem is when using this Html helper the hidden value is not updated on the re-post.
If you post something from the form and change it inside your controller, when you re-render the page using it will use the value which was originally posted to the action.
Instead use
<input type="hidden" name="ID" id="ID" value="#Html.Encode(Model.ID)" />

Resources