Upgrading data binding.

Posts   
 
    
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 08-Nov-2006 02:51:56   

First, the formalities.

LLBLGen Pro Version: 2.0.0.0 Final October 17th, 2006

ORMSuppportClasses.NET20.dll file version: 2.0.0.61005

Database: SQL Server 2000

I have upgrade from the prior version (1.0.2005.1) of the product and was able to make most of the translations easily enough. Changes to the Sort to the newer syntax, check. Using datatype? nullables, check. Using the template version of EntityCollection<EntityType>, check.

All that was pretty easy to figure out and update. My program seems to be working as it used to, except for a difference in the data binding behavior.

I am doing my data binding in code. The data is appearing from the database into the various controls, and I can even edit them and the data will save. So we are 90% of the way there without any changes to my databinding code.

However, I'm not sure how to handle this final 10%. The first issue is that I have code that looks like this:

m_currentDataPage.LoadData(policyTree.CurrentNodeId());
m_currentDataPage.Bind();
m_currentDataPage.DataChange += new EventHandler(m_Data_EntityContentsChanged);

The m_currentDataPage is the active derived tab control page. LoadData uses the node information and loads an entity based on the type of page m_currentDagePage is currently. Bind() then clears any existing data binding and binds all the controls to the page. Finally, I hook up an event that listens for the data being altered so I can present a "save" and "cancel" button.

In the older version, the bound data would throw this event and I was able to alter the UI as desired with the following:

internal void m_Data_EntityContentsChanged(object sender, EventArgs e)
{
  policyTree.Enabled = false;
  saveButton.Visible = true;
  undoChangesButton.Visible = true;
}

All this code does is disable the tree control that is used for navigation and makes the save and cancel buttons visible. Placing a breakpoint on the policyTree.Enabled line shows that this event is not triggering with the new LLBLGen entity being bound.

The second problem appears to be a binding change as well. In some cases, I wish to examine the data before handing it off and after edits are made. To do so, I use the following code:


binding = new Binding("Text", m_policy, "EstimatedPremium");
binding.Format += new ConvertEventHandler(Formatter.FormatCurrency);
PolicyPremium.DataBindings.Add(binding);

The event that gets called looks like:

public static void FormatCurrency(object sender, ConvertEventArgs e)
{
    decimal? currency = (decimal?) e.Value;
    if (currency.HasValue)
        e.Value = ((decimal)e.Value).ToString("C");
    else
        e.Value = "";
}

Again, in the old version of LLBLGen, these fired of fine. Now they fire off, but the data in "e.Value" is shown as "null" in the watch window. This is triggered when the focus leaves a control, and the value is "null" regardless of any data was placed in the input box that is bound to the data.

I read the breaking changes and I note that:

In the .NET 2.0 code, entity classes don't have the fieldChanged events, they implement the new .NET 2.0 interface INotifyPropertyChanged. This interface replaces the .NET 1.x events which were necessary for databinding. The interface INotifyPropertyChanged serves the same purpose: when a property changes, it raises an event and bound controls can update themselves if they're interested. This means that if you bind a property of an entity object to a textbox in your GUI, the textbox is automatically updated when the entity's field's value is changed using other code than the textbox. Every entity implements IEditableObject.

This would seem to indicate that the modernization affects data binding in the Entity->UI direction, but I'm getting the feeling that it also impacts it in the UI->Entity direction in a way that is interfering with my use of the change notification and formatting code. Any pointers for modernizing my code to match?

John

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 08-Nov-2006 09:21:58   

JohnL wrote:

First, the formalities.

LLBLGen Pro Version: 2.0.0.0 Final October 17th, 2006

ORMSuppportClasses.NET20.dll file version: 2.0.0.61005

Database: SQL Server 2000

I have upgrade from the prior version (1.0.2005.1) of the product and was able to make most of the translations easily enough. Changes to the Sort to the newer syntax, check. Using datatype? nullables, check. Using the template version of EntityCollection<EntityType>, check.

All that was pretty easy to figure out and update. My program seems to be working as it used to, except for a difference in the data binding behavior.

I am doing my data binding in code. The data is appearing from the database into the various controls, and I can even edit them and the data will save. So we are 90% of the way there without any changes to my databinding code.

However, I'm not sure how to handle this final 10%. The first issue is that I have code that looks like this:

m_currentDataPage.LoadData(policyTree.CurrentNodeId());
m_currentDataPage.Bind();
m_currentDataPage.DataChange += new EventHandler(m_Data_EntityContentsChanged);

The m_currentDataPage is the active derived tab control page. LoadData uses the node information and loads an entity based on the type of page m_currentDagePage is currently. Bind() then clears any existing data binding and binds all the controls to the page. Finally, I hook up an event that listens for the data being altered so I can present a "save" and "cancel" button.

In the older version, the bound data would throw this event and I was able to alter the UI as desired with the following:

internal void m_Data_EntityContentsChanged(object sender, EventArgs e)
{
  policyTree.Enabled = false;
  saveButton.Visible = true;
  undoChangesButton.Visible = true;
}

All this code does is disable the tree control that is used for navigation and makes the save and cancel buttons visible. Placing a breakpoint on the policyTree.Enabled line shows that this event is not triggering with the new LLBLGen entity being bound.

Why would you expect the event to happen? The event is only raised when the entity actually changes. As you just bind an entity, it's not changed. Or am I missing something?

In v2.0 some events are re-ordered as the code now supports .NET 2.0's property changed notifications. Still, if a property gets changed this event is raised.

The second problem appears to be a binding change as well. In some cases, I wish to examine the data before handing it off and after edits are made. To do so, I use the following code:


binding = new Binding("Text", m_policy, "EstimatedPremium");
binding.Format += new ConvertEventHandler(Formatter.FormatCurrency);
PolicyPremium.DataBindings.Add(binding);

The event that gets called looks like:

public static void FormatCurrency(object sender, ConvertEventArgs e)
{
    decimal? currency = (decimal?) e.Value;
    if (currency.HasValue)
        e.Value = ((decimal)e.Value).ToString("C");
    else
        e.Value = "";
}

Again, in the old version of LLBLGen, these fired of fine. Now they fire off, but the data in "e.Value" is shown as "null" in the watch window. This is triggered when the focus leaves a control, and the value is "null" regardless of any data was placed in the input box that is bound to the data.

I think I lack some contextual information here because I have no idea what kind of events you're using here. confused

Could it be that because of the change in events, no longer _entityfield_Changed events are raised, but a PropertyChanged event is raised ?

I read the breaking changes and I note that:

In the .NET 2.0 code, entity classes don't have the fieldChanged events, they implement the new .NET 2.0 interface INotifyPropertyChanged. This interface replaces the .NET 1.x events which were necessary for databinding. The interface INotifyPropertyChanged serves the same purpose: when a property changes, it raises an event and bound controls can update themselves if they're interested. This means that if you bind a property of an entity object to a textbox in your GUI, the textbox is automatically updated when the entity's field's value is changed using other code than the textbox. Every entity implements IEditableObject.

This would seem to indicate that the modernization affects data binding in the Entity->UI direction, but I'm getting the feeling that it also impacts it in the UI->Entity direction in a way that is interfering with my use of the change notification and formatting code. Any pointers for modernizing my code to match? John

The change notifications indeed might be the issue, as the 'fieldChanged' events aren't there anymore. What is a bit odd is that because they're not there, you should get compile errors IMHO, so it's odd you don't get these errors.

Also, you can also add this code in several other ways to the entities themselves. Of course, if you want it outside the entity, the notification event is a way to do it, however happens too late in the call chain IMHO to be very efficient. As the value has to be formatted, why not make the formatting a matter of the textbox and make the value of the textbox formatted already?

Frans Bouma | Lead developer LLBLGen Pro
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 08-Nov-2006 17:16:39   

Why would you expect the event to happen? The event is only raised when the entity actually changes. As you just bind an entity, it's not changed. Or am I missing something?

In the old code, this event (DataChange) is raised when the user leaves a control they changed. The old cycle appeared to be

UI edit->Parse Event->Bound entity data change->Format Event->Data Change event.

I think I lack some contextual information here because I have no idea what kind of events you're using here.

The Format event is triggered to change bound data into presented data. There is a Parse event that goes the other direction: allowing the program to strip optional formatting prior to attempting to store the input data text as a typed data.

As the value has to be formatted, why not make the formatting a matter of the textbox and make the value of the textbox formatted already?

That is what the Format event effectively does: it triggers when the bound data changes and applies the formatting so the textbox is updated to a formatted version. What appears to be happening differently is that in the old code, the cycle of events was as above. In the new code, the UI edit does trigger the parse event, but the bound data doesn't appear to be updating immediately after it triggers. What is odder is that then the Format event triggers (which should only happen after the bound data changes) and but it is passed a null.

JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 08-Nov-2006 19:55:27   

I did a bit of further digging. One of the value types that is presented by the system is percentages. The DB stores values such as "0.0200" while the UI displays such values as "2.00%". The SQL field is of numeric(5,4) form.

The mapping between the DB and UI is done via the Parse and Format events. The triggering of the events calls:

        public static void ParsePercent(object sender, ConvertEventArgs e)
        {
            string value = e.Value.ToString().Replace("%", "");
            double result;
            if ( double.TryParse(value, NumberStyles.Any, NumberFormatInfo.CurrentInfo, out result) )
                e.Value = (decimal) result/100;
        }

and

        public static void FormatPercent(object sender, ConvertEventArgs e)
        {
            decimal? percent = (decimal?)e.Value;
            if ( !percent.HasValue)
                e.Value = "0.00%";
            else
                e.Value = percent.Value.ToString("##0.00%");
        }

Once such Parse and Format pairs are written for a data type, I can use the old LLBLGen Entity model or even traditional DataSets with the code simply by hooking them up to the event in question.

I have yanked the new LLBLGen code and found that I can use both the older LLBLGen code and traditional DataSets with the Parse and Format (and DataChanged) events. The traceable events that I encounter when updating a field are:

Parse (which makes sense, as I'm providing new information which needs to be parsed prior to sending it to the bound field. The information in Parse is correct if the field already had a value and I'm replacing it.

After parse finishes, stepping the code takes me to getter of Nullable<System.Decimal> Probability

        public virtual Nullable<System.Decimal> Probability
        {
            get
            {
                object valueToReturn = base.GetCurrentFieldValue((int)PolicyProspectFieldIndex.Probability);

                return (Nullable<System.Decimal>)valueToReturn;
            }
            set { SetNewFieldValue((int)PolicyProspectFieldIndex.Probability, value); }
        }

The code returns the old value in the bound data. This is where the flow diverges from the DataSet or old LLBLGen sequence. In those, instances, the setter is called, updating the bound field to the parsed value. After the setter was called, the getter was then called, which then handed the value to Format and then to the TextBox.

I guess my question is how the LLBLGen 2.0 entity can avoid having the setter being called after the Parse event has modified e.Value to the desired stored value. The only variable is the type of entity bound to the field in question, no other code changes are necessary.

Even more puzzling is the result when the DB has a null stored in the field. In that case Parse is called with e.Value being null... not the value the user typed into the input field. If the DB has any other value, Parse gets called with e.Value being the information the user typed. Also, neither the getter NOR the setter for the field is called in this case.

Continuing to dig... text fields that don't have Format and Parse events seem to be working normally (well, except that such changes don't cause the DataChange event to raise).

JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 08-Nov-2006 22:40:14   

OK, I have created a simple application that distills the area down to raw essence:

I created a fresh SQL database. I then ran


CREATE TABLE [Bogus] (
    [BogusGuid]  uniqueidentifier ROWGUIDCOL  NOT NULL CONSTRAINT [DF_Bogus_BogusGuid] DEFAULT (newid()),
    [BogusPercent] [numeric](5, 4) NULL ,
    CONSTRAINT [PK_Bogus] PRIMARY KEY  CLUSTERED 
    (
        [BogusGuid]
    )  ON [PRIMARY] 
) ON [PRIMARY]
GO

insert into Bogus
(BogusGuid, BogusPercent)
values
('3DA80121-504B-46B3-A515-023665C539A3', 0.02)

This creates a single table with a single row.

Then I used both versions of LLBLGen to create a data access layer, simply adding this one table as an entity.

Finally, I created the apps themselves: (BogusApp1 is identical except for the namespace). The only difference between the applications is the LLBLGen they are linked to.

using System;
using System.Globalization;
using System.Windows.Forms;
using BogusOrm2.DatabaseSpecific;
using BogusOrm2.EntityClasses;

namespace BogusApp2
{
    public partial class Form1 : Form
    {
        private BogusEntity m_Bogus;
        public Form1()
        {
            InitializeComponent();

            //Hard coded single record for testing.
            m_Bogus = new BogusEntity(new Guid("3DA80121-504B-46B3-A515-023665C539A3"));
            
            using (DataAccessAdapter adapter = new DataAccessAdapter())
            {
                adapter.FetchEntity(m_Bogus);
            }

            Binding binding = new Binding("Text", m_Bogus, "BogusPercent");
            binding.Format += new ConvertEventHandler(FormatPercent);
            binding.Parse += new ConvertEventHandler(ParsePercent);
            
            bogusPercent.DataBindings.Add(binding);

        
        }

        public void FormatPercent(object sender, ConvertEventArgs e)
        {
            decimal? percent = (decimal?)e.Value;
            if ( !percent.HasValue)
                e.Value = "0.00%";
            else
                e.Value = percent.Value.ToString("##0.00%");
        }

        public void ParsePercent(object sender, ConvertEventArgs e)
        {
            string value = e.Value.ToString().Replace("%", "");
            double result;
            if (double.TryParse(value, NumberStyles.Any, NumberFormatInfo.CurrentInfo, out result))
                e.Value = (decimal)result / 100;
        }

        private void saveButton_Click(object sender, EventArgs e)
        {
            throw new NotImplementedException("No save implemented.");
        }
        
    }
}

BogusApp1 (linked to the older ORM) runs the conversion code in the Format and Parse as expected.

BogusApp2 (linked to the 2.0 version) runs the conversion code as previously described: the Parse event is triggered with the user's data, but the Format event fires off with the data in the Entity prior.

I think this is a simple as I can make the use of the conversion calls.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 09-Nov-2006 10:56:11   

Erm, isn't this databinding related and not llblgen pro related? ->

BogusApp2 (linked to the 2.0 version) runs the conversion code as previously described: the Parse event is triggered with the user's data, but the Format event fires off with the data in the Entity prior.

These events are raised by .NET's databinding framework, not llblgen pro. From the Binding.Format documentation:

The Format event occurs whenever the Current value of the BindingManagerBase changes, which includes:

  • The first time the property is bound.
  • Any time the Position changes.
  • Whenever the data-bound list is sorted or filtered, which is accomplished when a DataView supplies the list.

So of course Format is raised prior to parse, as it is raised the first time the property is bound.

So now why llblgen pro v1.0.2005.1 code works differently than the 2.0 code? Well, this is due to the fact that 1.0.2005.1 code has _entityField_Changed events, one for each property, and v2.0 implements INotifyPropertyChanged, which raises 1 event for all properties. I couldn't find a remark in the .NET docs about INotifyPropertyChanged and how it might change the order in which events are raised, however, it seems to me that there's only one situation explicitly described where an order of events is noticable, and that's when Parse is raised, it will always be followed by a Format.

I'll try to reproduce what you're running into, but I'm not sure what to look for: i.e.: a crash, an exception, odd behavior but the correct results etc.

(edit) the order I get: - Getter - Format - Getter - Format (databinding code likes to call things twice, why I have no idea).

Form shows.

I change 15.00% into 20.00% and TAB away to the button - Parse is called. - Format is called (this is documented, as quoted above from .NET docs. Format is always raised after Parse) - Setter is called (logical, the data goes down from textbox to field) - Getter is called, as the control has to be re-read because change notification was raised. (Callstack originates from EntityBase.OnPropertyChanged) - Format is called, logical, as getter is called so data comes up from field to control (same call stack) - Getter again (callstack originates completely from within control code, namely, from the propertydescriptor OnValueChanged... ) - Format again (follows Getter call) - form shows.

Frans Bouma | Lead developer LLBLGen Pro
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 13-Nov-2006 18:55:28   

form shows.

I'm confused: which value shows on your form? The newly entered value, or the value from the DB? The reason I ask is that I'm finding that the final format event call is pulling from an unaltered entity and the setter never fired. If your example code works, could you post or e-mail it... maybe something else I'm doing is mucking things up.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 14-Nov-2006 10:07:58   

I get 15.00% in the textbox. I change it to 25.00% and tab away to a button I have on the form. Value stays 25.00%. When I go back to the textbox it stays 25.00%.

Here's my code:

form startup.


[Test]
//[Ignore("Run manually")]
public void EventOrderTest()
{
    EventOrderTest tester = new EventOrderTest();
    tester.ShowDialog();
}

Form:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Northwind.Adapter.DatabaseSpecific;
using Northwind.Adapter.EntityClasses;
using System.Globalization;

namespace Unittests.TestLibrary.SqlServerTests.Adapter
{
    public partial class EventOrderTest : Form
    {
        private OrderDetailsEntity  _od;

        public EventOrderTest()
        {
            InitializeComponent();

            _od = new OrderDetailsEntity(10250, 51);

            using(DataAccessAdapter adapter = new DataAccessAdapter())
            {
                adapter.FetchEntity(_od);
            }

            Binding binding = new Binding("Text", _od, "Discount");
            binding.Format += new ConvertEventHandler(FormatPercent);
            binding.Parse += new ConvertEventHandler(ParsePercent);

            tbxDiscount.DataBindings.Add(binding);
        }


        public void FormatPercent(object sender, ConvertEventArgs e)
        {
            float? percent = (float?)e.Value;
            if(!percent.HasValue)
                e.Value = "0.00%";
            else
                e.Value = percent.Value.ToString("##0.00%");
        }

        public void ParsePercent(object sender, ConvertEventArgs e)
        {
            string value = e.Value.ToString().Replace("%", "");
            double result;
            if(double.TryParse(value, NumberStyles.Any, NumberFormatInfo.CurrentInfo, out result))
                e.Value = (float)result / 100f;
        }


        private void button1_Click(object sender, EventArgs e)
        {

        }
    }
}

Frans Bouma | Lead developer LLBLGen Pro
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 30-Nov-2006 19:09:33   

OK, after thinking that I was insane (because your code worked and mine didn't), I have finally nailed the problem down. First, the formatting was a red herring: it can be removed and the same effects can be seen. Second, the problem only occurs with fields defined as NULLABLE in the database. The new ORM binds great with NOT NULL fields, but my NULL fields call the GETTER when I make a change to the field and leave instead of the setter.

This can be seen by using your Northwind example code and altering the nullability in the Enterprise Manager of the Discount field and rebuilding the ORM. After doing so, the symptom I described will be exhibited. Returning the field back to its original state will allow the desired operation.

JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 30-Nov-2006 21:50:16   

An additional interesting note: if I build the ORM while the field is not null, and then change the field to null, the code works for non-null data. This would seem to indicate that the C# Nullable data types are not binding the same way as the base data types are.

Is there anywhere in the templates where a user can control the data binding process?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 01-Dec-2006 10:31:44   

So in short: if you have a field which is of type Nullable<T>, it doesn't work (i.e. you'll get the symptoms you described), and if the field is a normal type, e.g. int, it does work?

We've seen a lot of small problems with Nullable<T> and databinding, and in general I think that the last-minute change Microsoft made in .NET 2.0 to nullable types wasn't picked up by the trainwreck called Windows Forms.

Frans Bouma | Lead developer LLBLGen Pro
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 01-Dec-2006 16:50:58   

Otis wrote:

So in short: if you have a field which is of type Nullable<T>, it doesn't work (i.e. you'll get the symptoms you described), and if the field is a normal type, e.g. int, it does work?

We've seen a lot of small problems with Nullable<T> and databinding, and in general I think that the last-minute change Microsoft made in .NET 2.0 to nullable types wasn't picked up by the trainwreck called Windows Forms.

Exactly. If I build against a NOT NULL field (such as Northwind's Order Detail.Discount) your sample code and my code works and Nullable<T> isn't involved in the code. If I change that field to NULLable and refuse to rebuild the ORM, I can continue to work with the data. However, if I rebuild the ORM layer while the field is NULLable, I get the strange sequence from databinding using your example code and my own code that I described. It reliably (if incorrectly) is calling the GETTER of the Nullable<T> field after leaving an altered field instead of the SETTER.

This doesn't seem to impact char, nchar, varchar, nvarchar or text fields but seems to impact int, guid, float and numeric data types. I haven't tried the others yet. Perhaps the fact string types work has something to do with my binding to the "Text" property? I will have to verify that later.

As far as moving forward goes: I made my fields NOT NULL as this database isn't shared with any other applications. In fact, last night I even made "dummy" records in my lookup tables so NULL isn't necessary there either (although it is a bit awkward using 0 or '00000000-0000-0000-0000-000000000000' linked to a fake record, it works for this application). With those changes in place and the use of ProperyChanged the code appears to be fully converted. A few more days of testing and I may be able to release an updated version based on the current version of the ORM. I do very much like the strongly typed collections and the much faster compile times. I'm looking forward to exploring other areas I can use to clean up my code.

However, I do have other projects where I do not control the database schema as strongly and NULL is used fairly heavily. I'm glad I did my experimentation with this small project first. One thing I am exploring for my other projects is the use of the "GenerateNullableFieldsAsNullableTypes" option, which I presume uses the model from 1.1 for nullable fields?

JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 01-Dec-2006 17:03:20   

I can see that I misread what ConvertNulledReferenceTypesToDefaultValue did: it is the default setting and simply protects the code from seeing NULL references on reference types (which makes a lot of sense as a default).

Would it be possible to have a flag that would take that a step further and have a flag that generated value types as 1.1 did instead of Nullable<T> wrapped ones?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 01-Dec-2006 17:04:39   

There's a plugin shipped with the designer which does this for you. (Right mouse on 'Entities' -> run plugin). The preference is used for NEW fields, the plugin can be used for EXISTING fields simple_smile

Frans Bouma | Lead developer LLBLGen Pro
JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 01-Dec-2006 17:37:27   

Otis wrote:

There's a plugin shipped with the designer which does this for you. (Right mouse on 'Entities' -> run plugin). The preference is used for NEW fields, the plugin can be used for EXISTING fields simple_smile

That looks like an excellent compromise until Nullable<T> can be bound properly to WinForms controls.

JohnL
User
Posts: 47
Joined: 07-Oct-2005
# Posted on: 01-Dec-2006 17:50:31   

I just ran that to toggle them all to false in a test project... works as it should!