... ломать не строить ...
Итак, уважаемые, попытаемся разобраться в вопросе, как сделать свой
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; // ибо нефиг
}
}
Требуется:
- Уметь добавить строки <tr> в заголовок таблицы
- Уметь добавлять в заголовок <th>
- Уметь управлять свойствами
Rowspan, Colspan, Style, Width
и т.д.
- Уметь пропустить строку заголовка <tr> и добавить <th> в
следующую
Вариантов было два:
- Для каждого свойства добавить строковые параметры вида
THeader="text1, text2, text3" и получать из них массив параметров с
помощью String.Split
- Создать новый класс - "описатель заголовка" со свойствами 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 (как кистинный интерфейс) требует от нас описать следующие
методы :
- .ICollection.CopyTo(SystemArray, int)
- .ICollection.Count
- .ICollection.IsSynchronized
- .ICollection.SyncRoot
- .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 |
...........
Как обычно вариантов было два:
- ОДНА Table с < tr style="page-break-before=always"> и
размножением заголовков
- Вызывать несколько раз 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
(c)2002 Voennov Sergey aka Voennich |