Optional TopRow in NameValueList

Optional TopRow in NameValueList

Old forum URL: forums.lhotka.net/forums/t/862.aspx


DanEssin posted on Wednesday, August 09, 2006

For anyone interested, I've made a modification to NameValueList to allow the first item in the list to be a user-defined item like <no selection made> or blank or whatever. I have seen it in several other frameworks and I think that it's a useful additon. I've also added some code to the CodeSmith template to issue a select statement if the data source is a table or view. Since these lists are read-only and probably only read once, this minimizes the number of stored procs that you have to keep track of. I've attached the modified template which also fixes some bugs in TemplateBase.cs. This is version 0.9.5 of the CSLA 2.0 templates.

Sample Output
-------------------------------------
using System;
using System.Data;
using System.Data.SqlClient;
using Csla;
using Csla.Data;

namespace DM.Library
{
    [Serializable()]
    public class ProgramNameList : Csla.NameValueListBase<string, string>
    {

        #region Factory Methods
        private ProgramNameList()
        { /* require use of factory method */ }

        private static ProgramNameList _list;

        public static ProgramNameList GetProgramNameList()
        {
            if (_list == null)
                _list = DataPortal.Fetch<ProgramNameList>(new Criteria());
            return _list;
        }

        public static ProgramNameList GetProgramNameList(string topRowKey, string topRowValue)
        {
            if (_list == null)
                _list = DataPortal.Fetch<ProgramNameList>(new Criteria(topRowKey, topRowValue));
            return _list;
        }

        public static void InvalidateCache()
        {
            _list = null;
        }
        #endregion //Factory Methods


        #region Data Access
        #region Criteria
        [Serializable()]
        private new class Criteria
        {
            bool _addTopRow = false;
            string _topRowKey = "";
            string _topRowValue = "";
            public bool AddTopRow { get { return _addTopRow;} }
            public string TopRowKey { get { return _topRowKey;} }
            public string TopRowValue { get { return _topRowValue;} }

            public Criteria()
            {
            }
            public Criteria(string topRowKey, string topRowValue)
            {
                this._addTopRow = true;
                this._topRowKey = topRowKey;
                this._topRowValue = topRowValue;
            }
        }
        #endregion //Filter Criteria

        protected override void DataPortal_Fetch(Object criteria)
        {
            RaiseListChangedEvents = false;
            Criteria _criteria = (Criteria)criteria;
            using (SqlConnection cn = new SqlConnection(Database.CRMConnection))
            using (SqlCommand cm = cn.CreateCommand())
            {
                cn.Open();
                cm.CommandType = CommandType.Text;
                cm.CommandText = "SELECT ProgramName, ProgramName FROM Programs";

                using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))
                {
                    IsReadOnly = false;
                    if (_criteria.AddTopRow)
                         this.Add(new NameValuePair(_criteria.TopRowKey, _criteria.TopRowValue));
                    while(dr.Read())
                        this.Add(new NameValuePair(dr.GetString("ProgramKey"), dr.GetString("ProgramName")));
                    IsReadOnly = true;
                }
            }//using

            RaiseListChangedEvents = true;
        }
        #endregion //Data Access

    }
}

SteveChadbourne replied on Wednesday, August 09, 2006

I use NameValue lists with a top row quite a lot in my current application and find them very useful so your new template is gratefully received.

What would happen with your example code if I required a ProgramNameList on one screen with a top row but on another screen without one (a requirement for me) ? I would call ProgramNameList.GetProgramNameList("0", "None Selected") from the first screen which would load the list. On the second screen I would call ProgramNameList.GetProgramNameList() but as the list was already loaded and _list not null the re-load would not happen would it?

What I have done in this case is make _addTopRow a private static var of the ProgramNameList class and check it along with _list to determine if the list needs to be re-loaded.

Steve

DanEssin replied on Wednesday, August 09, 2006

That's a good idea. I'll give it a try. I was just going to create another one without the top item since it's going to be on a different form.

Dan

SteveChadbourne wrote:

I use NameValue lists with a top row quite a lot in my current application and find them very useful so your new template is gratefully received.

What would happen with your example code if I required a ProgramNameList on one screen with a top row but on another screen without one (a requirement for me) ? I would call ProgramNameList.GetProgramNameList("0", "None Selected") from the first screen which would load the list. On the second screen I would call ProgramNameList.GetProgramNameList() but as the list was already loaded and _list not null the re-load would not happen would it?

What I have done in this case is make _addTopRow a private static var of the ProgramNameList class and check it along with _list to determine if the list needs to be re-loaded.

Steve




DanEssin replied on Wednesday, August 09, 2006

I've revised the template to incorporate your suggestion.

Dan

SteveChadbourne wrote:

I use NameValue lists with a top row quite a lot in my current application and find them very useful so your new template is gratefully received.

What would happen with your example code if I required a ProgramNameList on one screen with a top row but on another screen without one (a requirement for me) ? I would call ProgramNameList.GetProgramNameList("0", "None Selected") from the first screen which would load the list. On the second screen I would call ProgramNameList.GetProgramNameList() but as the list was already loaded and _list not null the re-load would not happen would it?

What I have done in this case is make _addTopRow a private static var of the ProgramNameList class and check it along with _list to determine if the list needs to be re-loaded.

Steve




DanEssin replied on Wednesday, August 09, 2006

Here it is.

SteveChadbourne replied on Thursday, August 10, 2006

I can't seem to get it to work. I tried to create a NameValueList from the new template, pointed it at a stored procedure that returns a result set with Id and Name fields and get the following error:

System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
   at System.Collections.ArrayList.get_Item(Int32 index)
   at CodeSmith.Csla.TemplateBase.ObjectInfo.get_Inherits() in c:\dotNet2.0\csla 2.0cs\codesmith 0.9.5\TemplateBase.cs:line 1180
   at _CodeSmith.NameValueList_cst.__RenderMethod1(TextWriter writer, Control control) in c:\dotNet2.0\csla 2.0cs\codesmith 0.9.5\NameValueList.cst:line 30
   at CodeSmith.Engine.DelegateControl.Render(TextWriter writer)
   at CodeSmith.Engine.Control.RenderChildren(TextWriter writer)
   at CodeSmith.Engine.CodeTemplate.Render(TextWriter writer)
   at CodeSmith.Engine.CodeTemplate.RenderToString()
   at CodeSmith.Gui.CodeTemplateGenerator.f(Object A_0, EventArgs A_1)

DanEssin replied on Thursday, August 10, 2006

I couldn't reproduce the error you got but I did find a bug which I fixed. My SP for testing is:

CREATE PROCEDURE [dbo].[GetNameValueTest] AS
SELECT ResultID, ResultName from DmResults
GO

It looks like I can only attach 1 file so I've added a screen shot of the property grid filled out in a way that works with the above SP and the revised template and does not give me an error message.

SteveChadbourne replied on Thursday, August 10, 2006

Generates fine now thanks. Code looks good. I'll let you know when I've had a chance to test it.

Is it worth passing addTopRow into InvalidateCache ? Makes the factory methods a bit tidier:

  public static MyObect GetMyObect()
  {
   if (_addTopRow)
    InvalidateCache(false);
   if (_list == null)
    _list = DataPortal.Fetch<MyObect>(new Criteria());
   return _list;
  }

  public static MyObect GetMyObect(string topRowKey, string topRowValue)
  {
   if (!_addTopRow)
    InvalidateCache(true);
   if (_list == null)
    _list = DataPortal.Fetch<MyObect>(new Criteria(topRowKey, topRowValue));
   return _list;
  }

  public static void InvalidateCache(bool addTopRow)
  {
   _list = null;
   _addTopRow = addTopRow;
  }

DanEssin replied on Thursday, August 10, 2006

I thought that I could infer it from which overload was being called, the argument-less call clearly wants it to be false and the argument call clearly wants it to be true, so I just tested for a state change. What do you think?

SteveChadbourne wrote:

Generates fine now thanks. Code looks good. I'll let you know when I've had a chance to test it.

Is it worth passing addTopRow into InvalidateCache ? Makes the factory methods a bit tidier:

  public static MyObect GetMyObect()
  {
   if (_addTopRow)
    InvalidateCache(false);
   if (_list == null)
    _list = DataPortal.Fetch<MyObect>(new Criteria());
   return _list;
  }

  public static MyObect GetMyObect(string topRowKey, string topRowValue)
  {
   if (!_addTopRow)
    InvalidateCache(true);
   if (_list == null)
    _list = DataPortal.Fetch<MyObect>(new Criteria(topRowKey, topRowValue));
   return _list;
  }

  public static void InvalidateCache(bool addTopRow)
  {
   _list = null;
   _addTopRow = addTopRow;
  }




SteveChadbourne replied on Thursday, August 10, 2006

Yes you can infer the new _addTopRow state from which overload you are in. My code offering was just an attempt at making the state change test a bit tidier. Your way works fine.

DanEssin replied on Thursday, August 10, 2006

I agree that your's is tidier.

SteveChadbourne wrote:

Yes you can infer the new _addTopRow state from which overload you are in. My code offering was just an attempt at making the state change test a bit tidier. Your way works fine.




rasupit replied on Thursday, August 10, 2006

I've attached the modified template which also fixes some bugs in TemplateBase.cs. This is version 0.9.5 of the CSLA 2.0 templates.

I appreciate if you can report back the bugs that you found.  It will help me stabilize them quicker.

Thanks,

Ricky

DanEssin replied on Friday, August 11, 2006

i posted them to your bug tracker.

Dan

rasupit wrote:

I've attached the modified template which also fixes some bugs in TemplateBase.cs. This is version 0.9.5 of the CSLA 2.0 templates.

I appreciate if you can report back the bugs that you found.  It will help me stabilize them quicker.

Thanks,

Ricky




rasupit replied on Friday, August 11, 2006

Dan,

Thanks for the bug reports, I'll take a look and have them fixed.

Ricky

tetranz replied on Sunday, August 13, 2006

I handle this sort of thing with something I've built that I call a HeadedBindinglist. Its along the same lines (although simpler) as the SortedBindingList. It takes any List<T> and an additional instance of T for the header item. The result is something that looks like the original List<T> with the extra item at the top.

Behind the scenes, all it really does is fiddle with the index and provide an enumerator. If you request index 0, it returns the header item, if you request any other index, you get the item which is at index-1 in the underlying list. This lets you provide a "pure" list from your middle tier with the HeadedBindingList being just a UI thing. You can very economically have multiple HeadedBindingLists for the same List<T> with different header items. Neither the list items nor the list itself is duplicated, its really just pointers. I haven't tried it but it should be able to be chained with the SortedBindingList or FilteredBindingList.

I can probably post the code next week if anyone is interested. Without being too immodest, I think it is a good solution to this age old, somewhat awkward requirement.

Cheers
Ross

SteveChadbourne replied on Monday, August 14, 2006

Yes, I would be interested in seeing it.

McManus replied on Tuesday, August 15, 2006

Ross,

I like the concept of HeadedBindingList being just a UI thing. Sounds like it's exactly the thing I'm looking for. So yes, I'm very interested in your code...

Cheers,

Herman

lioncall replied on Tuesday, August 15, 2006

Hi,

In case it's applicable. If you using asp.net 2.0, on ListControl base class there is a property AppendDatBoundItems you can consider using:

<asp:DropDownList ID="DropDownList1" AppendDataBoundItems="true" runat="server" DataSourceID="SqlDataSource1" DataTextField="state" DataValueField="state">

    <asp:ListItem Text="(Select a State)" Value="" />   

</asp:DropDownList>

 

Thanks,

tetranz replied on Tuesday, August 15, 2006

HeaderedBindingList.zip is attached to this message. I think you need to go to the forum site to get it. Its exactly what I'm using in production except that I've changed the NameSpace.

Sorry its not really documented but I just told Visual Studio to implement the interfaces and then I filled in only the methods that it really needed.  The resulting list is read only although it does respond to ListChanged events in the underlying list. See the SortedBindingList in Rocky's book for further info.

The contructor has two overloads. One takes an array of headers (so you can have more than one) and the other takes a single object.

Use it like this: Assuming myList is the underlying list containing instances of MyItem which takes, for example, an Id and Name.

HeaderedBindingList listForDropdown = new HeaderedBindingList(myList, new MyItem(0, "Select Item"));

I tend to make a singleton with it. ie, this sort of thing.

HeaderedBindingList _listForDropdown = null;

public HeaderedBindingList ListForDropdown
{
    get
    {
       if (_listForDropdown == null)
        {
            _listForDropdown = new HeaderedBindingList(myList, new MyItem(0, "Select Item"));
        }
       return _listForDropdown;
    }
}

Back in the middle tier, myList itself is probably a similar singleton and may be be used for things other than just the dropdown. Those things don't want to see the header item(s).

Cheers
Ross

McManus replied on Tuesday, August 15, 2006

Ross,

Thanks for posting the HeaderedBindingList code. I played a bit with it and it is exactly what I was looking for!!

Cheers,

Herman

ajj3085 replied on Tuesday, August 15, 2006

Ross,

This looks awesome, I'll certainly use it whenever I need this functionality.  One question though:  Does it play nice with sortedbindinglist?  (haven't looked at the code, been busy).

Andy

tetranz replied on Thursday, August 17, 2006

Hi Andy

I haven't tried it with SortedBindingList so I don't know the answer. I think it should work.

I see now that I left out the test in the constructor to see if the source list is a BindingList or just an IList. I've obviously only used it with BindingLists. It should really have that test like SortedBindingList does.

Ross

ajj3085:
Ross,

This looks awesome, I'll certainly use it whenever I need this functionality.  One question though:  Does it play nice with sortedbindinglist?  (haven't looked at the code, been busy).

Andy

JHurrell replied on Thursday, August 17, 2006

Ross,

That really is a good idea. In the past, I've done what I'm sure a lot of other people do and overload a list to accept a header item.

Coming up with a new UI object to encapsulate the header and the original list is definitely an improved pattern.

Thanks for sharing.

- John

Gareth replied on Wednesday, June 13, 2007

Has anyone managed to get the HeaderedBindingList working with a NameValueListBase?

I've got a business object:

public class UserList : NameValueListBase<int, string>
{
    ...
        public static UserList GetList()
        {
            if (_list == null)
                _list = DataPortal.Fetch<UserList>
                  (new Criteria(typeof(UserList)));
            return _list;
        }
    ...
        public static NameValuePair NullUser()
        {
            return new NameValuePair(0, string.Empty);
        }

In my .aspx code I have the following:

    private NetShare.Library.UserList GetUserList()
    { ...  }

    protected void UserListDataSource_SelectObject(object sender, Csla.Web.SelectObjectArgs e)
    {
        NetShare.Library.UserList obj = GetUserList();
        HeaderedBindingList<NetShare.Library.UserList> hbl =
            new HeaderedBindingList<NetShare.Library.UserList>(hbl, NetShare.Library.UserList.NullUser);
        e.BusinessObject = hbl;       
    }

Unfortunately I don't think I'm fully understanding the model as I'm getting the following errors:

Error    1    The best overloaded method match for 'HeaderedBindingList<NetShare.Library.UserList>.HeaderedBindingList(System.Collections.Generic.IList<NetShare.Library.UserList>, NetShare.Library.UserList[])' has some invalid arguments   

Error    2    Argument '2': cannot convert from 'method group' to 'NetShare.Library.UserList[]'   

Can anyone point me in the right direction?

Thanks,
Gareth.

McManus replied on Wednesday, June 13, 2007

Gareth,

The code should look something like:

protected void UserListDataSource_SelectObject(object sender, Csla.Web.SelectObjectArgs e)
{
     NetShare.Library.UserList obj = GetUserList();
     HeaderedBindingList<NetShare.Library.UserList.NameValuePair> hbl =
        new HeaderedBindingList<NetShare.Library.UserList.NameValuePair>(obj, NetShare.Library.UserList.NullUser);
    e.BusinessObject = hbl;
}

Cheers,
Herman

alef replied on Tuesday, March 04, 2008

I'm trying to implement a NVL of active lookup codes.

So when adding a new row the NVL only has Active records in it.

When editing an existing record it is the list of Active records plus the related record if it isinactive.

I'm using HeaderedBindingList to extend the active values for a combo with the current value for the object (which can be an inactive value)

In the code below

* ColorList.GetColorList() gives me all the active colors.

*_originalColorLID and _originalColorName are variables with the original values from the database.

 

#region ValidColorList

[NonSerialized()]

[NotUndoable()]

private HeaderedBindingList<ColorList.NameValuePair> _validColorList = null;

public HeaderedBindingList<ColorList.NameValuePair> ValidColorList

{

get

{

if (_validColorList == null)

{

if (ColorList.GetColorList().ContainsKey(_originalColorLID))

{

_validColorList = new HeaderedBindingList<ColorList.NameValuePair>(ColorList.GetColorList(), new ColorList.NameValuePair[0]);

}

else

{

_validColorList = new HeaderedBindingList<ColorList.NameValuePair>(ColorList.GetColorList(), new ColorList.NameValuePair(_originalColorLID, _originalColorName));

}

}

return _validColorList ;

}

}

#endregion

 

 

Now I want to implement the following property but it gives me an error

public string ColorName

{

get

{

CanReadProperty("ColorName" true);

if (_colorLID == _originalColorLID)

{

return _originalColorName;

}

else

{

return ((ColorList)ValidColorList).GetItemByKey(_colorLID).Value;    //=> this gives an error

}

return _colorName;

}

 

Cannot convert type 'Company.Library.Helper.HeaderedBindingList<Csla.NameValueListBase<int,string>.NameValuePair>' to 'Company.Library.ColorList'

 

Can you help me with the right syntax? I want to lookup the value in the ValidColorList for a certain key.

 

I’ve tried also

return ((NameValueListBase<int,string>)ValidKleurList).GetItemByKey(_kleurLID).Value;

but this gives also an error.

 

 

damo replied on Tuesday, March 04, 2008

For those of you who are considering doing this in the business logic and not the UI, in DevExpress land (and I think Infragistics) have a concept in their lookup controls call NullText which is the text to display when the property being bound to is null. So I would agree this is a UI / display formatting issue and adding items into your list is not really the way to go.

For this to work, the corresponding databound property on your business object will have to be nullable.

alef replied on Wednesday, March 05, 2008

 

 

 

If you want to show a blank line at the GUI then it is indeed a responsibility for the GUI.

For that the GUI has two choices:

1) With the DevExpress LookUpEdit control the user can use Ctrl+Del to clear the value.

With the property NullText we can set a text that is displayed when the column contains a null reference.

2) The GUI can add an extra row in the LookUpEdit control because the user doesn’t like Ctrl+Del. This extra row can be either a blank row or a row that shows some text like “[blank]”, etc. Therefore, if a user wants to select nothing, he could just select this extra row.

This feature is the responsibility for the GUI, it has to create a new list, which always return an empty row as the first item.

 

But in the case I’m describing in my opion it is the responsibilty of the business to deliver the valid list for the GUI (all the active codes + the current code even it is inactive for the concerned record)

 

This design discussion I find also interesting. But I want to know also the technical answer.

How can I find an item in the list wrapped with the hepler class HeaderedBindingList.

In the NVL list of the CSLA framework there exist a method GetItemByKey.

Could this be possible with the HeaderedBindingList by casting it to a NVL. I don’t think this is possible because it is a wrapper around the NVL and it is not subclassed from NVL.

So how can I have the same possibilities with the HeaderedBindingList as the NVL?

Copyright (c) Marimer LLC