Mark ItemViewModel as dirty explicitly - tornadofx

I have address editor fragment, after user edit address I save changed model values to a database
fun saveAddressTable(tableViewEditModel: TableViewEditModel<Client>) {
tableViewEditModel.items.asSequence()
.filter { it.value.isDirty }
.forEach {
//do stuff }
}
Some adressess have obvious mistakes I can fix programmatically, however model is not marked as dirty after I change it.
For example,
fun autoEditAddressTable(tableViewEditModel: TableViewEditModel<Client>) {
tableViewEditModel.items.asSequence()
.forEach {
val client = it.value.item
client.localProperty.value = client.local.replace(",{2,}".toRegex(), ",")
}
}
Changes reflected in the UI, but model itself is not dirty. I've found markDirty() property method, but it doesn't help much. As model is not marked dirty, it fails filter criteria on save.
center = tableview ( controller.clients ) {
column("Local Address", Client::localProperty)
.prefWidth(400.0)
.makeEditable()
bindSelected(controller.clientModel)
enableDirtyTracking()
tableViewEditModel = editModel
}
Isn't TableViewEditModel expected to trace all model alteration? If client edits Local Address column manually, model becomes dirty.

Related

TornadoFX - how to fix ListView with a custom cell factory not updating properly when items are deleted?

I have a ListView displaying custom objects from my domain model, and if I use a custom cell factory to display the objects' properties in each row of the list, I get strange behaviour when I delete items. If the item is not the last in the list, the deleted item remains visible and the last item disappears. However, the item has been removed from the backing list as expected, and attempting to delete the phantom object has no further effect.
The display seems not to be refreshing properly, because after some arbitrary resizing of the window, the list eventually refreshes to its expected values. I've tried calling refresh() on the ListView manually but it has no noticeable effect.
Removing my custom cell factory fixes the problem, and I've seen other posts that have had a similar problem using standard JavaFX (ListView using custom cell factory doesn't update after items deleted) where the problem is fixed by changing the implementation of updateItem(Object item, boolean empty), but I can't work out how to do that in TornadoFX.
Here's an example that demonstrates the update issue (but not the phantom item, that only happens if the delete button is part of the custom cell):
package example
import javafx.scene.control.ListView
import tornadofx.*
data class DomainClass(val name: String, val flag1: Boolean, val flag2: Boolean, val info: String)
class UpdateIssue : App(UpdateIssueView::class)
class UpdateIssueView : View() {
val listSource = mutableListOf(
DomainClass("object1", true, false, "more info"),
DomainClass("object2", false, true, "even more info"),
DomainClass("object3", false, false, "all the info")
).observable()
var lst: ListView<DomainClass> by singleAssign()
override val root = vbox {
lst = listview(listSource) {
cellFormat {
graphic = cache {
hbox {
textfield(it.name)
combobox<Boolean> {
selectionModel.select(it.flag1)
}
combobox<Boolean> {
selectionModel.select(it.flag2)
}
textfield(it.info)
}
}
}
}
button("delete") {
action {
listSource.remove(lst.selectedItem)
}
}
}
}
Any help greatly appreciated!
The suggestion from #Edvin Syse to remove the cache block fixed this for me (although note that he also said a more performant fix would be to implement a ListCellFragment, which I haven't done here):
....
lst = listview(listSource) {
cellFormat {
graphic = hbox {
textfield(it.name)
combobox<Boolean> {
selectionModel.select(it.flag1)
}
combobox<Boolean> {
selectionModel.select(it.flag2)
}
textfield(it.info)
}
}
}
I noticed that the ComboBoxes don't show any other selectable values besides it.flag1 and flag2. You'll want to set the values property to true/false or true/false/null. You can then set the value item directly.
lst = listview(listSource) {
cellFormat {
graphic = hbox {
textfield(it.name)
combobox(values=listOf(true, false)) {
value = it.flag1
}
combobox(values=listOf(true, false)) {
value = it.flag2
}
textfield(it.info)
}
}
}

Why session.getSaveBatch() is undefined when child record was added - Ext 5.1.1

Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.

Passing in LUIS entities to bind to dialog state

Can somebody help me interpret what the heck this means from the bot framework documention:
You can also pass in LUIS entities to bind to the state. If the EntityRecommendation.Type is a path to a field in your C# class then the EntityRecommendation.Entity will be passed through the recognizer to bind to your field. Just like initial state, any step for filling in that field will be skipped.
When I call my dialog I pass in my LuisResult result Entities collection like so:
context.Call(new FormDialog<ItemSearch>( new ItemSearch(), ItemSearch.BuildForm, options: FormOptions.PromptInStart,entities:result.Entities), null);
Within those entities is at least one which maps in both name and type to a public property on my dialog however the state never gets filled. What am I missing?
TIA.
You can find an example of this in the PizzaOrderDialog. if you look at FormDialog implementation, it is using the entity.type to map the passed in entity recommendation to a step in the form. Then the detected entities will be provided as an input to that step of the form.
Here is an example of how form can skip the kind step based on the detected entities by Luis model in pizza form:
var entities = new List<EntityRecommendation>(result.Entities);
if (!entities.Any((entity) => entity.Type == "Kind"))
{
// Infer kind
foreach (var entity in result.Entities)
{
string kind = null;
switch (entity.Type)
{
case "Signature": kind = "Signature"; break;
case "GourmetDelite": kind = "Gourmet delite"; break;
case "Stuffed": kind = "stuffed"; break;
default:
if (entity.Type.StartsWith("BYO")) kind = "byo";
break;
}
if (kind != null)
{
entities.Add(new EntityRecommendation(type: "Kind") { Entity = kind });
break;
}
}
}
var pizzaForm = new FormDialog<PizzaOrder>(new PizzaOrder(), this.MakePizzaForm, FormOptions.PromptInStart, entities);
It also appears that there is an issue with passing Entities in. It seems to work if the property you are mapping to is a Enum (as per the PizzaBot sample). However if the public property is a string, it doesn't map. I'm not sure about other types.
See here https://github.com/Microsoft/BotBuilder/issues/151

How do I delete records from a child collection in LINQ to SQL?

I have two tables in my database connected by foreign keys: Page (PageId, other data) and PageTag (PageId, Tag). I've used LINQ to generate classes for these tables, with the page as the parent and the Tag as the child collection (one to many relationship). Is there any way to mark PageTag records for deletion from the database from within the Page class?
Quick Clearification:
I want the child objects to be deleted when the parent DataContext calls SubmitChanges(), not before. I want TagString to behave exactly like any of the other properties of the Page object.
I would like to enable code like the following:
Page page = mDataContext.Pages.Where(page => page.pageId = 1);
page.TagString = "new set of tags";
//Changes have not been written to the database at this point.
mDataContext.SubmitChanges();
//All changes should now be saved to the database.
Here is my situation in detail:
In order to make working with the collection of tags easier, I've added a property to the Page object that treats the Tag collection as a string:
public string TagString {
get {
StringBuilder output = new StringBuilder();
foreach (PageTag tag in PageTags) {
output.Append(tag.Tag + " ");
}
if (output.Length > 0) {
output.Remove(output.Length - 1, 1);
}
return output.ToString();
}
set {
string[] tags = value.Split(' ');
PageTags.Clear();
foreach (string tag in tags) {
PageTag pageTag = new PageTag();
pageTag.Tag = tag;
PageTags.Add(pageTag);
}
}
}
Basically, the idea is that when a string of tags is sent to this property, the current tags of the object are deleted and a new set is generated in their place.
The problem I'm encountering is that this line:
PageTags.Clear();
Doesn't actually delete the old tags from the database when changes are submitted.
Looking around, the "proper" way to delete things seems to be to call the DeleteOnSubmit method of the data context class. But I don't appear to have access to the DataContext class from within the Page class.
Does anyone know of a way to mark the child elements for deletion from the database from within the Page class?
After some more research, I believe I've managed to find a solution. Marking an object for deletion when it's removed from a collection is controlled by the DeleteOnNull parameter of the Association attribute.
This parameter is set to true when the relationship between two tables is marked with OnDelete Cascade.
Unfortunately, there is no way to set this attribute from within the designer, and no way to set it from within the partial class in the *DataContext.cs file. The only way to set it without enabling cascading deletes is to manually edit the *DataContext.designer.cs file.
In my case, this meant finding the Page association, and adding the DeleteOnNull property:
[Association(Name="Page_PageTag", Storage="_Page", ThisKey="PageId", OtherKey="iPageId", IsForeignKey=true)]
public Page Page
{
...
}
And adding the DeleteOnNull attribute:
[Association(Name="Page_PageTag", Storage="_Page", ThisKey="PageId", OtherKey="iPageId", IsForeignKey=true, DeleteOnNull = true)]
public Page Page
{
...
}
Note that the attribute needed to be added to the Page property of the PageTag class, not the other way around.
See also:
Beth Massi -- LINQ to SQL and One-To-Many Relationships
Dave Brace -- LINQ to SQL: DeleteOnNull
Sorry, my bad. That won't work.
It really looks like you need to be doing this in your repository, rather than in your Page class. There, you have access to your original data context.
There is a way to "attach" the original data context, but by the time you do that, it has become quite the code smell.
Do you have a relationship, in your Linq to SQL entity diagram, linking the Page and PageTags tables? If you don't, that is why you can't see the PageTags class from the Page class.
If the foreign key in the PageTags database table is set to Allow Nulls, Linq to SQL will not create the link when you drag the tables into the designer, even if you created a relationship on the SQL Server.
This is one of those areas where OR mapping can get kind of hairy. Providing this TagString property makes things a bit more convenient, but in the long run it obfuscates what is really happening when someone utilizes the TagString property. By hiding the fact that your performing data modification, someone can very easily come along and set the TagString without using your Page entity within the scope of a DataContext, which could lead to some difficult to find bugs.
A better solution would be to add a Tags property on the Page class with the L2S model designer, and require that the PageTags be edited directly on the Tags property, within the scope of a DataContext. Make the TagString property read only, so it can be genreated (and still provide some convenience), but eliminate the confusion and difficulty around setting that property. This kind of change clarifies intent, and makes it obvious what is happening and what is required by consumers of the Page object to make it happen.
Since Tags is a property of your Page object, as long as it is attached to a DataContext, any changes to that collection will properly trigger deletions or insertions in the database in response to Remove or Add calls.
Aaron,
Apparently you have to loop thru your PageTag records, calling DeleteOnSubmit for each one. Linq to SQL should create an aggregate query to delete all of the records at once when you call SubmitChanges, so overhead should be minimal.
replace
PageTags.Clear();
with
foreach (PageTag tag in PageTags)
myDataContext.DeleteOnSubmit(tag);
Aaron:
Add a DataContext member to your PageTag partial class.
partial class PageTag
{
DataClassesDataContext myDataContext = new DataClassesDataContext();
public string TagString {
..etc.
Larger code sample posted at Robert Harvey's request:
DataContext.cs file:
namespace MyProject.Library.Model
{
using Tome.Library.Parsing;
using System.Text;
partial class Page
{
//Part of Robert Harvey's proposed solution.
MyDataContext mDataContext = new TomeDataContext();
public string TagString {
get {
StringBuilder output = new StringBuilder();
foreach (PageTag tag in PageTags) {
output.Append(tag.Tag + " ");
}
if (output.Length > 0) {
output.Remove(output.Length - 1, 1);
}
return output.ToString();
}
set {
string[] tags = value.Split(' ');
//Original code, fails to mark for deletion.
//PageTags.Clear();
//Robert Harvey's suggestion, thorws exception "Cannot remove an entity that has not been attached."
foreach (PageTag tag in PageTags) {
mDataContext.PageTags.DeleteOnSubmit(tag);
}
foreach (string tag in tags) {
PageTag PageTag = new PageTag();
PageTag.Tag = tag;
PageTags.Add(PageTag);
}
}
}
private bool mIsNew;
public bool IsNew {
get {
return mIsNew;
}
}
partial void OnCreated() {
mIsNew = true;
}
partial void OnLoaded() {
mIsNew = false;
}
}
}
Repository Methods:
public void Save() {
mDataContext.SubmitChanges();
}
public Page GetPage(string pageName) {
Page page =
(from p in mDataContext.Pages
where p.FileName == pageName
select p).SingleOrDefault();
return page;
}
Usage:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(string pageName, FormCollection formValues) {
Page updatedPage = mRepository.GetPage(pageName);
//TagString is a Form value, and is set via UpdateModel.
UpdateModel(updatedPage, formValues.ToValueProvider());
updatedPage.FileName = pageName;
//At this point NO changes should have been written to the database.
mRepository.Save();
//All changes should NOW be saved to the database.
return RedirectToAction("Index", "Pages", new { PageName = pageName });
}

linqToSql related table not delay loading properly. Not populating at all

I have a couple of tables with similar relationship structure to the standard Order, OrderLine tables.
When creating a data context, it gives the Order class an OrderLines property that should be populated with OrderLine objects for that particular Order object.
Sure, by default it will delay load the stuff in the OrderLine property but that should be fairly transparent right?
Ok, here is the problem I have: I'm getting an empty list when I go MyOrder.OrderLines but when I go myDataContext.OrderLines.Where(line => line.OrderId == 1) I get the right list.
public void B()
{
var dbContext = new Adis.CA.Repository.Database.CaDataContext(
"<connectionString>");
dbContext.Connection.Open();
dbContext.Transaction = dbContext.Connection.BeginTransaction();
try
{
//!!!Edit: Imortant to note that the order with orderID=1 already exists
//!!!in the database
//just add some new order lines to make sure there are some
var NewOrderLines = new List<OrderLines>()
{
new OrderLine() { OrderID=1, LineID=300 },
new OrderLine() { OrderID=1, LineID=301 },
new OrderLine() { OrderID=1, LineID=302 },
new OrderLine() { OrderID=1, LineID=303 }
};
dbContext.OrderLines.InsertAllOnSubmit(NewOrderLines);
dbContext.SubmitChanges();
//this will give me the 4 rows I just inserted
var orderLinesDirect = dbContext.OrderLines
.Where(orderLine => orderLine.OrderID == 1);
var order = dbContext.Orders.Where(order => order.OrderID == 1);
//this will be an empty list
var orderLinesThroughOrder = order.OrderLines;
}
catch (System.Data.SqlClient.SqlException e)
{
dbContext.Transaction.Rollback();
throw;
}
finally
{
dbContext.Transaction.Rollback();
dbContext.Dispose();
dbContext = null;
}
}
So as far as I can see, I'm not doing anything particularly strange but I would think that orderLinesDirect and orderLinesThroughOrder would give me the same result set.
Can anyone tell me why it doesn't?
You're just adding OrderLines; not any actual Orders. So the Where on dbContext.Orders returns an empty list.
How you can still find the property OrderLines on order I don't understand, so I may be goofing up here.
[Edit]
Could you update the example to show actual types, especially of the order variable? Imo, it shoud be an IQueryable<Order>, but it's strange that you can .OrderLines into that. Try adding a First() or FirstOrDefault() after the Where.

Resources