Home > Mobile >  ASP.NET WebForms UserControl with generic type parameter
ASP.NET WebForms UserControl with generic type parameter

Time:12-21

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?

CodePudding user response:

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), BindingFlags.Public | BindingFlags.Static)
          .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(new[] { 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 is object ? value.ToArray() : null;
  }
}
  • Related