1

In Visual Studio 2022 ASP.NET Web Application Project (.NET Framework) I seemed to be able to make a User Control with generics:

public class StronglyTypedListBox<T> : System.Web.UI.UserControl {
  protected ListBox lst; // This line actually exists in the .ascx.designer.cs file.
                         // I list it here for brevity.
  public GuardedList<T> Items {
    /* GuardedList<T> derives from System.Collections.ObjectModel.Collection<T>.
       lst.Items will be automatically populated and updated whenever Items are modified. */
    get { ... }
    set { ... }
  }
  /* Other constructs providing functionalities such as removing an item and moving an item up/down */
}

, and the .ascx markup:

<%@ Control ...... Inherits="MyProject.StronglyTypedListBox<T>" %>
<asp:ListBox id="lst" runat="server"></asp:ListBox>
<%-- Other markup providing functionalities such as removing an item and moving an item up/down --%>

To use this "generic" user control in a page, I dragged the .ascx file into the page markup (just like how we add any UserControl into a page), and changed its id to lst.

And I moved the following line from the .aspx.designer.cs file

protected StronglyTypedListBox<T> lst;

to the .aspx.cs file and modified it as:

protected StronglyTypedListBox<OneOfMyEntityType> lst;

All above seemed to be OK in Visual Studio. No red squiggles and the project builds with no error messages. But when pressing F5 the page shows exception saying it fails parsing line 1 of the .ascx because it couldn't load the MyProject.StronglyTypedListBox<T> type.

Is what I want to achieve with the above codes possible? If yes, what needs to be fixed?

Jyunhao Shih
  • 1,287
  • 2
  • 8
  • 9
  • I don't see how ANY approach to software development suggests that after you compile code, you THEN go change some bytes in the compiled output. And I don't see how ANY practical software development can suggest and adopt the idea that after you build your project, you have to go modify the desinger.cs file. Really, any solution that centers around such a paradigm is doomed - 100% doomed. What is worse, is dropping that control into any web page will of course THEN generate an new designer bit of code for that given page. Really, regardless of whatever solution you adopt, this idea is no go. – Albert D. Kallal Dec 08 '22 at 16:37
  • @AlbertD.Kallal I'm afraid you missunderstood something... Nothing is changed after compilation/build. All steps I described is design time, in source codes. .aspx.designer.cs is part of source codes, and in it the comments Visual Studio generates say you can move the automatically generated field declaration (which represents a child control) from .aspx.designer.cs to .aspx.cs and modify it if what VS generated doesn't suit your needs. – Jyunhao Shih Dec 08 '22 at 22:47
  • Ok, fair enough - it just that I don't want one to have such changes overwriting - but at the end of the day, I not really helping your post anyway - my apologies. Perhaps allowing a parameter or method that accepts a type of object would work here. – Albert D. Kallal Dec 09 '22 at 01:22
  • @AlbertD.Kallal I thought of that to -- giving up making the user control generic, instead adding a Type property to it to receive type information... But as the codes in my post show, the user control uses GuardedList. I don't know how to convert a Type object to a generic type param so that GuardedList can use it... – Jyunhao Shih Dec 09 '22 at 03:29

1 Answers1

0

As discussed in comments, quit generics becasue aspnet_compiler.exe simply doesn't support it. Instead have a Type property and leverage reflection, which is the key to the solution.

For example, below is a user control containing a ListBox (named lst) with its ListItems mapped with a collection (defined as Items property) of a specific type (defined as ItemType property). That is, the ListItems of the ListBox are automatically populated/added/removed according to Items, and after postback Items can be rebuilt from the ListItems.

public partial class MappedListBox : System.Web.UI.UserControl {

  public Type ItemType {
    get => this._ItemType;
    set {
      if(this._ItemType != value && this._Items is object)
        throw new InvalidOperationException();
      this._ItemType = value;
    }
  }

  public Func<object, ListItem> ItemToListItem { get; set; }

  public Func<ListItem, object> ListItemToItem { get; set; }

  public IList Items {
    set {
      this.Items.Clear();
      if(value is object) foreach(var item in value) this.Items.Add(item);
    }
    get {
      if(this._Items is null) {
        if(this.ItemType is null) throw new ArgumentNullException(nameof(ItemType));

        //Recover entity objects from ListBox.Items as an IEnumerable<ItemType>
        var initialItems =
          typeof(Enumerable)
          .GetMethod(nameof(Enumerable.Cast))
          .MakeGenericMethod(this.ItemType)
          .Invoke(
            null
            , new[] {
                this.lst.Items.Cast<ListItem>().Select(i => this.ListItemToItem(i))
              }
          );
        
        //Rebuild this._Items as a GuardedList<ItemType>, filling it with initialItems,
        //and specify that addition/removal inside this list should automatically add/remove corresponding ListItems.
        //(This capability comes from System.Collections.ObjectModel.Collection<T>, which GuardedList<T> inherits.)
        var typeOfGuardedListOfItemType =
          typeof(GuardedList<>).MakeGenericType(this.ItemType);
        var guardedList =
          Activator.CreateInstance(
            typeOfGuardedListOfItemType
            , initialItems
            , (Action<int, object>)(
                (index, item) =>
                  this.lst.Items.Insert(index, this.ItemToListItem(item))
              )
            , (Action<int>)(index => this.lst.Items.RemoveAt(index))
            , (Action)(() => this.lst.Items.Clear())
            , (Action<int, object>)(
                (index, item) => {
                  var listItem = this.ItemToListItem(item);
                  this.lst.Items[index].Value = listItem.Value;
                  this.lst.Items[index].Text = listItem.Text;
                }
              )
          );
        this._Items = (IList)guardedList;
      }
      return this._Items;
    }
  }

  /* Constructs providing other functionalities such as moving an item up/down and so on */

  Type _ItemType;

  IList _Items;
}

And it can be used in a Page or another user control like this:

public partial class EmployeeSelection : System.Web.UI.UserControl {
  // The MappedListBox instance is named lstSelected

  protected override void OnInit(EventArgs e) {
    this.lstSelected.ItemType = typeof(Employee);
    this.lstSelected.ItemToListItem = item => {
      var employee = (Employee)item;
      return new ListItem(employee.Name, employee.ID);
    };
    this.lstSelected.ListItemToItem = listItem => DB.Employees.Find(listItem.Value);
    
    this.EmployeeTreeView.SelectedNodeChanged += (s, ev) =>
      this.SelectedEmployees.Add(getChosenEmployeeFromTreeView());

    base.OnInit(e);
  }

  internal IList<Employee> SelectedEmployees {
    get => (IList<Employee>)this.lstSelected.Items;
    set => this.lstSelected.Items = value?.ToArray();
  }
}
Jyunhao Shih
  • 1,287
  • 2
  • 8
  • 9