Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - ASP.NET »

Обсудить на форуме Обсудить на форуме

Custom DataGrid

... ломать не строить ...
Итак, уважаемые, попытаемся разобраться в вопросе, как сделать свой Column и свой DataGrid?
Зачем это нужно или откуда растут ноги? Все очень просто:
в DataGrid можно сделать так: но весьма затруднительно сделать так:

Однако, в старом-добром asp нужный результат получался просто элементарно.
Примечание : это - результат запроса "Sales By Year" к стандартной базе Northwind
 

Начнем !

создадим два пустых класса
 
    
public class Colmn: System.Web.UI.WebControls.BoundColumn 
{ 
  ///  <SUMMARY>
  /// Модифицированный BoundColumn 
  /// </SUMMARY>
  public Colmn() 
  { 
  } 
} 
public class RegularTable : System.Web.UI.WebControls.DataGrid 
{ 
  ///  <SUMMARY>
  /// Модифицированная DataGrid 
  /// </SUMMARY>
  public RegularTable() 
  { 
    this.AutoGenerateColumns= false; // ибо нефиг 
  } 
}
    
Требуется:
  1. Уметь добавить строки <tr> в заголовок таблицы
  2. Уметь добавлять в заголовок <th>
  3. Уметь управлять свойствами Rowspan, Colspan, Style, Width и т.д.
  4. Уметь пропустить строку заголовка <tr> и добавить <th> в следующую
Вариантов было два:
  1. Для каждого свойства добавить строковые параметры вида THeader="text1, text2, text3" и получать из них массив параметров с помощью String.Split
  2. Создать новый класс - "описатель заголовка" со свойствами HText, HRowspan..; описать коллекцию таких классов и добавить эту коллекцию к нашему столбцу
Более расширяемым, интересным и аккуратным является вариант №2, на нем я и остановился.
 
Шаг 1
добавим в Источник Данных доп столбец "_counter" (зачем он нужен, покажу ниже)
Шаг 2

добавляем счетчик кол-ва строк в заголовке int _headerRowCount; (наполнять мы его будем чуть позже)
начинаем вмешиваться в Render(); создаем свой CustomRender();
научимся находить наши столбцы в коллекции в CustomRender();
Добавим в RegularTable метод AddColumnTo(Object oc, DataGridItem di, int pos)
Добавим в Colmn метод bool HeaderExists(int) - который будет отвечать, нужна ли в данной строке ячейка данного заголовка
добавим в Colmn метод ToHeaderCell() на выходе которого будем ловить готовый TableHeaderCell
Стандартные столбцы будем растягивать на все строки заголовка, устанавливая RowSpan = _headerRowCount
в общем получаем:

public class  Colmn: System.Web.UI.WebControls.BoundColumn
{
  /// <summary>
  /// Модифицированный BoundColumn
  /// </summary>
  private int _colspan = 0;
  private int _rowspan = 0;
  private Unit _width;
  private Unit _height;

  public Colmn()
  {
  }
  public bool HeaderExists(int num)
  {
    try
    {
    if   
      (num==0) 
        return true;
      return false;
    }
    catch(Exception e)
    {
      throw new Exception(" Error in Colmn.HeaderExists num="+num+" headerCount=",e);
    }
  }

  public TableHeaderCell ToHeaderCell(int num)
  {
    if(num>=0)
    {
      if(num==0)
      {
        TableHeaderCell  th = new TableHeaderCell();
        th.Text  = base.HeaderText;
        th.ControlStyle.CopyFrom(base.HeaderStyle);          
        if (this._colspan>1)
          th.ColumnSpan =  this._colspan;
        if (this._rowspan>1)
          th.RowSpan = this._rowspan;
        th.Width = this._width;
        return th;
      }
      else
      {
        //TODO коллекция столбцов 
        return null;
      }
    }
    else
      throw new Exception("Cannot make TableHeaderCell at Pos #"+num+" header.Count=");
  }
#region public Properties 
  public int ColSpan
  {
    get {return _colspan; }
    set { if (value>0) _colspan = value; }
  }
  public int RowSpan
  {
    get {return _rowspan; }
    set { if (value>0) _rowspan = value; }
  }
  public Unit Width
  {
    get {return _width; }
    set { _width = value; }
  }
  public Unit Height
  {
    get {return _height; }
    set { _height = value; }
  }
#endregion
}



public class RegularTable : System.Web.UI.WebControls.DataGrid
{
  /// <summary>
  /// Модифицированная DataGrid 
  /// </summary>
  private int _headerRowCount;
  
  public RegularTable()
  {
    this.AutoGenerateColumns=false; // ибо нефиг 
  }
    private void AddColumnTo(Object oc, DataGridItem di, int pos)
  {
    Colmn c;
    TableHeaderCell th;
    DataGridColumn dc;
    if (oc.GetType() != typeof(kcs.RegularTable.Colmn)  )
    {
      if (pos == 0) 
      {
        // это стандартный DataGrid`овский столбец - 
        //можем поставить его только в первую строку 
        
        dc = (DataGridColumn)oc;
        th = new TableHeaderCell();
        th.Text = ( (DataGridColumn)oc).HeaderText;
        if (dc.HeaderStyle != null)
          th.ControlStyle.CopyFrom( dc.HeaderStyle );
          
        // "обычный" столбец будем вытягивать на весь заголовок
        th.RowSpan = _headerRowCount; 
        di.Controls.Add(th);
      }
    }
    else // а это наш Colmn
    { 
      c = (Colmn)oc;
      if (c.HeaderExists( pos ))
        di.Controls.Add( ((Colmn)oc).ToHeaderCell( pos ) );
    }
  }
  public void CustomRender(HtmlTextWriter output)
  {
    TableHeaderCell th;
    int thRowCount = 0;// счетчик номера строки заголовка
    foreach(Control cc in Controls)
      if ( (cc.HasControls() ) && (this.ShowHeader ) )
      {
        #region добавляем недостающие строки
        try
        {
          for(int i =0; i < _headerRowCount - 1 ; i++ )
          {
            DataGridItem dgi = new DataGridItem(-1, 0, ListItemType.Header );
            cc.Controls.AddAt(i+1, dgi );
          }
        }
        catch(Exception e )
        {
          Page.Trace.Warn("_RegularTable : CustomRender","new DataGridItem", e);
        }
        #endregion
        foreach(DataGridItem di in cc.Controls ) 
        {
          if (di.ItemType == ListItemType.Header ) 
          {
            // очищаем строку заголовка от ячеек
            if(di.Cells.Count > 0 )
              di.Cells.Clear(); 
            foreach(Object oc in Columns)
              AddColumnTo(oc, di, thRowCount);
            thRowCount++;
          }
        }
      }

    base.Render(output);
  }
  protected override void Render(HtmlTextWriter output)
  {
    _headerRowCount=1;
    foreach(Object oc in Columns)
    {
      if (oc.GetType() == typeof(kcs.RegularTable.Colmn) ) 
      { // наш столбец ! 
        Colmn c = (Colmn)oc;
        if (_headerRowCount < c.RowSpan )
          _headerRowCount = c.RowSpan; 
      }
    }
    
    CustomRender(output);
  }
  public override object DataSource
  {
    get { return base.DataSource ; }

    set 
    { 
      DataView tdw;
      DataTable tdt = null;

      Page.Trace.Write("_RegularTable : DataSource"," "+value.GetType() );
      try
      {
        if (value.GetType() == typeof(System.Data.DataView) )
        {
          Page.Trace.Write("_RegularTable : DataSource"," Its DataView ! " );
          tdw = (DataView) value;
          tdt = tdw.Table;
        }
        if (value.GetType() == typeof(System.Data.DataTable) )
        {
          tdt = (DataTable) value;
          tdt = tdt; 
        }
        if (tdt != null)
        {
          DataColumn dc = new DataColumn();
          dc.ColumnName = "_counter";
          if (!tdt.Columns.Contains("_counter"))
            tdt.Columns.Add(dc);
          tdt.AcceptChanges();
          int i=1;
          foreach(DataRow dr in tdt.Rows)
            dr["_counter"]=i++;
          base.DataSource = tdt;
        }
        else
        {
          Page.Trace.Warn("_RegularTable : DataBind", 
          "Cannot undestand DataSource "+value.GetType()+" Use default");
          base.DataSource = value; 
        }
      }
      catch(Exception e)
      {
        Page.Trace.Warn("_RegularTable : DataBind", "copy Error",e);
      }
    }
  }
}
    
Итак мы уже можем управлять свойствами Rowspan, Colspan, Width, Height
Шаг 3
Пора задуматься о классе "описатель заголовка" - так его и назовем.
Собственно говоря повторяем то, что уже сделано
свойства Text, ColSpan, RowSpan, Width, Height, Style.
и метод ToTH() возвращающий TableHeaderCell
public class HeaderDef
{
  /// <summary>
  /// Элемент - описатель заголовка таблицы
  /// </summary>
  private string _text;
  private int _colspan = 0;
  private int _rowspan = 0;
  private Unit _width;
  private Unit _height;
  private TableItemStyle _style;
  private bool _exists = true;
  
#region Properties - доступ к Private свойствам
  public string Text
  {
    get {return _text;}
    set {_text = value;}
  }
  public TableItemStyle Style
  {
    get 
    {
      if ( _style == null)
        _style = new TableItemStyle();
      return _style;
    }
  }
  public int ColSpan
  {
    get {return _colspan; }
    set { if (value>0) _colspan = value; }
  }
  public int RowSpan
  {
    get {return _rowspan; }
    set { if (value>0) _rowspan = value; }
  }
  public bool Exists
  {
    get { return _exists; }
    set { _exists = value; }
  }
  public Unit Width
  {
    get {return _width; }
    set { _width = value; }
  }
  public Unit Height
  {
    get {return _height; }
    set { _height = value; }
  }

#endregion
  override public string ToString()
  {
    return "Header Text:"+_text;
  }
  
  public TableHeaderCell ToTH()
  {
    TableHeaderCell th = new TableHeaderCell();
    th.Text = this._text;
    if (this._colspan>1)
      th.ColumnSpan = this._colspan;
    if (this._rowspan>1)
      th.RowSpan = this._rowspan;
    if (this._style!=null)
      th.ControlStyle.CopyFrom(this._style);
    if (this._exists)
      return th;
    else
      return null;
  }
}
    
Шаг 4
Теперь нам нужно сделать Коллекцию HeaderDefCollection : ICollection
определим массив HeaderDef, минимальный шаг увеличения массива
( что бы не перетряхивать его каждый раз при добавлении элемента)
и переменную которая будет хранить реальное кол-во элементов.
ICollection (как кистинный интерфейс) требует от нас описать следующие методы :
  1. .ICollection.CopyTo(SystemArray, int)
  2. .ICollection.Count
  3. .ICollection.IsSynchronized
  4. .ICollection.SyncRoot
  5. .IEnumerable.GetEnumerator()
певые четыре не представляют никаких проблем,
а для GetEnumerator прийдется создать класс HeaderDefEumerator() : IEnumerator
и реализовать методы .Current, .MoveNext(), .Reset()
так же для описания метода .MoveNext() добавим метод GetElement(int) в класс HeaderDefCollection
несмотря на то что мы не описывали HeaderDefCollection как IList
добавим два метода Add и this[i]
Получим :
public class HeaderDefCollection : ICollection
{
  private HeaderDef[] _array;
  private int _size;
  private const int _MinStep =5;
  
  public HeaderDefCollection()
  {
    _array = new HeaderDef[_MinStep];
    _size = 0;
  }

# region из ICollection 
  public virtual void CopyTo(Array array, int index)
  {
    throw new Exception("Sorry It doesn`t work");
  }
  public virtual int Count
  {
    get {return _size; }
  }
  public virtual bool IsSynchronized 
  {
    get { return false; } 
  }
  public virtual Object SyncRoot
  {
    get { return this; }
  }
  internal Object GetElement(int i)
  {
    if ((i>0) && (i<=_size)) 
      return _array[i];
    else
      return null;
  }

  public virtual IEnumerator GetEnumerator()
  {
    return new HeaderDefEnumerator(this);
  }

  class HeaderDefEnumerator : IEnumerator
  {
    private HeaderDefCollection _col;
    private int _index;
    private Object _current;

    internal HeaderDefEnumerator( HeaderDefCollection hc )
    {
      _col = hc;
      _index = 0;
      _current = _col._array;
      if (_col._size == 0)
        _index = -1;
    }

    public  bool MoveNext() 
    {
      if (_index < 0) 
      {  
        _current = _col._array;
        return false;
      }
      _current = _col.GetElement(_index);
      _index++;

      if (_index == _col._size)
        _index = -1;
      return true;
    }

    public void Reset() 
    {

      if (_col._size == 0)
        _index = -1;
      else
        _index = 0;

      _current = _col._array;
    }

    public Object Current 
    {
      get 
      {
        if (_current == _col._array)
        {
          if (_index == 0)
            throw new InvalidOperationException("Invalid Operation");
          else
            throw new InvalidOperationException("Invalid operation");
        }
        return _current;
      }
    }
  }
#endregion
  public int Add(object obj)
  {
    HeaderDef hd = (HeaderDef) obj;
    HeaderDef[] newarray;
    int capacity;
    if( _size == _array.Length )
    {
      capacity = _array.Length + _MinStep;
      newarray = new HeaderDef[capacity];
      Array.Copy(_array,newarray,_size);
      _array = newarray;
    }
    _array[_size] = (HeaderDef)obj;
    _size++;
    return _size-1;
  }
  public Object this[int num]
  {
    get
    {
      if  ( (num>=0) && (num<_size) )
        return this._array[num];
      else
        return null;
    }
    set{}
  }
}
    
Шаг 5
остается добавить коллекцию HeaderDefCollection к классу Colmn,
поправить методы HeaderExists(), ToHeaderCell();
для подсчета кол-ва строк заголовка поправим RegularTable.Render добавим property Colmn.HeaderCount
протестируем (прим - испольуется вспомогательный класс AdoNet ) на примере Test2.aspx:
<%@Import namespace="System.Web" %>
<%@Import namespace="System.Data" %>
<%@Import namespace="System.Data.SqlClient" %>

<!- пользовательские библиотеки-->
<%@Import namespace="kcs.AdoNet" %>
<%@Import namespace="kcs.RegularTable" %>
<%@Register TagPrefix="kcs" Namespace="kcs.RegularTable" Assembly="RegularTable"%>

<HTML>
<head>

   <meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<script runat="server">

void Page_Load()
{
  string sd, ed;
  sd = Request.Form.GetValues("StartDate")[0];
  ed = Request.Form.GetValues("EndDate")[0];
  
  AdoNet T = new AdoNet();
  T.CacheTime = 200;
  T.ScriptTimeout = 300;
  T.sql = "Sales By Year";
  T.AddParam("@Beginning_Date", sd.ToString() );
  T.AddParam("@Ending_Date", ed.ToString() );


  T.ExecuteDS();
  MainTable.DataSource = T[0];
  MainTable.DataBind();
}


</script>
</head>

<BODY>
<kcs:RegularTable id = MainTable 
  AutogenerateColumns = false
  runat = server>
  <Columns>
    <kcs:Colmn DataField = "_counter" HeaderText = "N" RowSpan=2/>
    <kcs:Colmn DataField = "OrderId" >
      <headers>
        <kcs:HeaderDef Text="Заказ" ColSpan=2 />
        <kcs:HeaderDef Text="ID"/>
      </headers>
    </kcs:Colmn>
    <kcs:Colmn DataField = "ShippedDate"  >
      <headers>
        <kcs:HeaderDef Exists = false />
        <kcs:HeaderDef Text = "Дата" />
      </headers>
    </kcs:Colmn>
    <asp:BoundColumn DataField = "Subtotal" HeaderText = "Сумма"
      DataFormatString="{0:n2}"
      ItemStyle-HorizontalAlign="Right"
    />
  </Columns>
</kcs:RegularTable>

</BODY>
</HTML>
    
получим более чем приятную для нас картинку :

Шаг 6
.. аппетит приходит во время еды или чем дальше в лес тем толще глюки
Остался один незаконченный вопрос - хочется Paging, но не тот Paging,
кототорый показывает нам MS, а возможность нарезать таблицу на куски.
Как Вы, вероятно, догадались нужно это для печати документов,
а куски таблиц должны иметь разный (задаваемый размер)
и между ними нужно уметь вставить некоторые элементы
Примерно так :

Заголовок НА ПЕРВОЙ СТРАНИЦЕ
Заголовок 1 Заголовок 2
Значение 1.1 Значение 2.1
..........
Значение 1.N Значение 2.N

подпись под таблицей
--- Переход на новую страницу, например с помощью<br style="page-break-before: always"> ---
Заголовок над таблицей
 
Заголовок 1 Заголовок 2
Значение 1.N+1 Значение 2.N+1
..........
Значение 1.N+K Значение 2.N+K

...........

Как обычно вариантов было два:
  1. ОДНА Table с < tr style="page-break-before=always"> и размножением заголовков
  2. Вызывать несколько раз base.Render, задавая разные DataSource
для меня более привлекательным оказался второй вариант по тому,
что вариант №1 накладывает ограничения на стили, используемые в таблице
и не позволяет вставить произвольное наполнение между страницами.
именно для этого мне и требовалось автодобавление столбца _counter
Добавим нумератор страниц int[] _pagenum; и свойства:
#region 
операции с нумерацией страниц public 
string PageNum {
set
  {
    string
    [] tmpstr; tmpstr
    = value.Split(new char[]{','}); _pagenum
    = new int[tmpstr.Length]; for(int
    i= 0;i< tmpstr.Length; i++)
    {
      int crezult;
      crezult = Convert.ToInt32(tmpstr[i]);
      if (crezult>0)
        _pagenum[i] = crezult;
      else
        _pagenum[i] = 0;
    }
  }
}
private int GetPageNum(int num) 
{
  if ( (num>=0) && (num < _pagenum.Length) )
    return _pagenum[num];
  else
    return _pagenum[_pagenum.Length-1];
}
#endregion
    
добавим в конструктор
_pagenum = new int[1];
_pagenum[0] = 0;
    
и исправим Render таким образом:
if (_pagenum[0]==0)
{
  base.DataBind();
  CustomRender(output);
  return;
}
DataTable dt = (DataTable) base.DataSource;

if (dt!=null) 
{

  int LastRow;
  bool PrintFooter;
  PrintFooter = base.ShowFooter;
  LastRow = dt.Rows.Count;
  DataView dw;
  dw = dt.DefaultView;
  base.ShowFooter = false;
  for(int i = 0, cpCount = 0 ; i < LastRow; i += this.GetPageNum(cpCount), cpCount++)
  {
    dw.RowFilter="(_counter>" + i + ") AND (_counter <= " + ( i + this.GetPageNum(cpCount) )+")";
    base.DataSource = dw;
    if (LastRow - i < this.GetPageNum(cpCount + 1) )
      base.ShowFooter = PrintFooter;
    base.DataBind();
    CustomRender(output);
    if (LastRow-i > this.GetPageNum(cpCount + 1) )
      output.Write(TableBreaker);
  }
}
else
{
  //Page.Trace.Warn("_RegularTable : Render" , "DataTable not Found");
  throw new Exception("_RegularTable : Render DataTable not Found");
}
    

используем полученные свойства:
........    
<BODY>
Бааальшой заголовок
<kcs:RegularTable id = MainTable 
  PageNum = "2, 10"
  TableBreaker=" подпись < hr& gt; Заголовок! "
  runat = server>
  <Columns>
    <kcs:Colmn DataField = "_counter" HeaderText = "N" RowSpan=2/>
    <kcs:Colmn DataField = "OrderId" >
      <headers>
        <kcs:HeaderDef Text="Заказ" ColSpan=2 />
        <kcs:HeaderDef Text="ID"/>
      </headers>
    </kcs:Colmn>
    <kcs:Colmn DataField = "ShippedDate"  >
      <headers>
        <kcs:HeaderDef Exists = false />
        <kcs:HeaderDef Text = "Дата" />
      </headers>
    </kcs:Colmn>
    <asp:BoundColumn DataField = "Subtotal" HeaderText = "Сумма"
      DataFormatString="{0:n2}"
      ItemStyle-HorizontalAlign="Right"
    />
  </Columns>
</kcs:RegularTable>
......
    
и вот что мы получили:

Послесловие
Данный проект можно и нужно развивать дальше,
например превратив TableBreaker и PageNum в отдельный класс и сделать из них коллекцию,
добавить возможность подключения данных к ним....
о всех качественных изменениях я постараюсь рассказать так же подробно

...Продолжение следует...
исходники : RegularTable AdoNet используемые Asp и Aspx
полный код последнего варианта RegularTable
(c)2002 Voennov Sergey aka Voennich


Может пригодится:


Автор: Voennov Sergey
Прочитано: 4549
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру