Sunday, 28 July 2013

Tweaking the GridView Control Adaptor for Bootstrap

To create this project we are going to get a bit of a leg up, we are going to take as our starting point the GridVew Adaptor from the ASP.NET 2.0 CSS Friendly Control Adapters 1.0 source code here CSS Friendly Control Adapters now the last update was Mar 5, 2007 so it was not created for .Net 4, 4.5 or even 4.5.1 but since we know it works after a fashion from my previous blog post we know we can use the GirdView adaptor as least as a starting point.

Note: Just a note I will be giving credit to the original Control Adapters from ASP.Net 2.0 when i publish this project on Codeplex and NuGet.org
Updated: As of the writing of this article Bootstrap 3 RC1 was released so I will be referencing that documentation and moving this project to that release moving forward it will be released via NuGet anyway.

Getting setup

GetTheGridViewAdapter

Figure 1 – Get the GridViewAdapter and helper files and the JavaScript file

We will start with the project from the last post if you followed all we need to do is remove the references to the old CSS Friendly Control Adapters and add a reference to our BootstrapFriendlyControlAdapters give the application a run and here we are

TheDefaultGridViewAdapterInUse

Figure 2 – the default GridView Adapter in use

As you will see the pager is not displaying correctly so lets fix that. In Figure 3 we can see the issues highlighted, so what I did was remove the outer DIV and change the class for each of the inner SPANs (see Figure 4) I have added the class form-inline to both spans and to the right span which we want to be pulled left I have added the  pull-left class.

Pager With Issues

Figure 3 – pager with issues

Pager With Issues Fixed

Figure 4 – pager with issues fixed

Next we need to re-write some of the WritePagerSection method Listing 1.

/// <remarks>
/// Patch provided by Wizzard to support PagerTemplate (CodePlex issue #3368).
/// </remarks>
private void WritePagerSection(HtmlTextWriter writer, PagerPosition pos)
{
GridView gridView = Control as GridView;
if ((gridView != null) &&
gridView.AllowPaging &&
gridView.PagerSettings.Visible &&
(gridView.PageCount > 1) &&
((gridView.PagerSettings.Position == pos) || (gridView.PagerSettings.Position == PagerPosition.TopAndBottom)))
{
GridViewRow pagerRow = (pos == PagerPosition.Top) ? gridView.TopPagerRow : gridView.BottomPagerRow;
string className = GetRowClass(gridView, pagerRow);
className += " AspNet-GridView-" + (pos == PagerPosition.Top ? "Top " : "Bottom ");

//check for PagerTemplate
if (gridView.PagerTemplate != null)
{
if (gridView.PagerStyle != null)
{
className += gridView.PagerStyle.CssClass;
}
className = className.Trim();

writer.WriteLine();
writer.WriteBeginTag("div");
writer.WriteAttribute("class", className);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

if (pagerRow != null)
{
foreach (TableCell cell in pagerRow.Cells)
{
foreach (Control ctrl in cell.Controls)
{
ctrl.RenderControl(writer);
}
}
}

writer.Indent--;
writer.WriteEndTag("div");
}
else //if not a PagerTemplate
{
Table innerTable = null;
if ((pos == PagerPosition.Top) &&
(gridView.TopPagerRow != null) &&
(gridView.TopPagerRow.Cells.Count == 1) &&
(gridView.TopPagerRow.Cells[0].Controls.Count == 1) &&
typeof(Table).IsAssignableFrom(gridView.TopPagerRow.Cells[0].Controls[0].GetType()))
{
innerTable = gridView.TopPagerRow.Cells[0].Controls[0] as Table;
}
else if ((pos == PagerPosition.Bottom) &&
(gridView.BottomPagerRow != null) &&
(gridView.BottomPagerRow.Cells.Count == 1) &&
(gridView.BottomPagerRow.Cells[0].Controls.Count == 1) &&
typeof(Table).IsAssignableFrom(gridView.BottomPagerRow.Cells[0].Controls[0].GetType()))
{
innerTable = gridView.BottomPagerRow.Cells[0].Controls[0] as Table;
}

if ((innerTable != null) && (innerTable.Rows.Count == 1))
{
if (gridView.PagerStyle != null)
{
className += gridView.PagerStyle.CssClass;
}
className = className.Trim();

writer.WriteLine();
writer.WriteBeginTag("div");
writer.WriteAttribute("class", className);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

TableRow row = innerTable.Rows[0];
foreach (TableCell cell in row.Cells)
{
foreach (Control ctrl in cell.Controls)
{
writer.WriteLine();
ctrl.RenderControl(writer);
}
}

writer.Indent--;
writer.WriteLine();
writer.WriteEndTag("div");
}
}
}
}
Listing 1 – WritePagerSection method

One of the main issues and it’s to my shame Embarrassed smile I originally wrote this section. So here is the issue I am surrounding the pagers with a DIV instead of the correct section this means that the

<table>
<thead>
...
</thead>
<tbody>
...
</tbody>
<tfoot>
<tr> --footer goes here-- </tr>
</tfoot>
</table>

Listing 2 -

The Default GridView Adapter In Use Issue Div And Position Issue

Figure 5 - The Default GridView Adapter In Use DIV and Position Issue

There are two issues here the first is that the pager is positioned outside the table when its rendered and second it’s in a DIV as mentioned above so we need to modify this section to place the pager inside the table and in side a TFOOT section when at the bottom and a THEAD when at the top.

Move Header Pager Inside Table

Figure 6 – Move Header Pager Inside Table

Move Footer Pager Inside Table

Figure 7 – Move Footer Pager Inside Table

/// <remarks>
/// Patch provided by Wizzard to support PagerTemplate (CodePlex issue #3368).
/// </remarks>
private void WritePagerSection(HtmlTextWriter writer, PagerPosition pos)
{
GridView gridView = Control as GridView;
string tableHeaderFooter = pos == PagerPosition.Top ? "thead" : "tfoot";
string tableCell = pos == PagerPosition.Top ? "th" : "td";

if ((gridView != null) && gridView.AllowPaging && gridView.PagerSettings.Visible && (gridView.PageCount > 1) &&
((gridView.PagerSettings.Position == pos) || (gridView.PagerSettings.Position == PagerPosition.TopAndBottom)))
{
GridViewRow pagerRow = (pos == PagerPosition.Top) ? gridView.TopPagerRow : gridView.BottomPagerRow;
string className = GetRowClass(gridView, pagerRow);
//className += " " + (pos == PagerPosition.Top ? "Top " : "Bottom ");

//check for PagerTemplate
if (gridView.PagerTemplate != null)
{
if (gridView.PagerStyle != null)
{
className += gridView.PagerStyle.CssClass;
}
className = className.Trim();

// get pager section type
writer.WriteLine();
writer.WriteBeginTag(tableHeaderFooter); // change div to tfoot
writer.WriteAttribute("class", className);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

// add opening row
writer.WriteBeginTag("tr");
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

// add opening cell
writer.WriteBeginTag(tableCell);
// add colspan to cell
writer.WriteAttribute("colspan", gridView.HeaderRow.Cells.Count.ToString());
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

if (pagerRow != null)
{
foreach (TableCell cell in pagerRow.Cells)
{
foreach (Control ctrl in cell.Controls)
{
ctrl.RenderControl(writer);
}
}
}

// add closing cell
writer.Indent--;
writer.WriteEndTag(tableCell);

// add closing row
writer.Indent--;
writer.WriteEndTag("tr");

writer.Indent--;
writer.WriteEndTag(tableHeaderFooter); // change div to tfoot
}
else //if not a PagerTemplate
{
Table innerTable = null;
if ((pos == PagerPosition.Top) &&
(gridView.TopPagerRow != null) &&
(gridView.TopPagerRow.Cells.Count == 1) &&
(gridView.TopPagerRow.Cells[0].Controls.Count == 1) &&
typeof(Table).IsAssignableFrom(gridView.TopPagerRow.Cells[0].Controls[0].GetType()))
{
innerTable = gridView.TopPagerRow.Cells[0].Controls[0] as Table;
}
else if ((pos == PagerPosition.Bottom) &&
(gridView.BottomPagerRow != null) &&
(gridView.BottomPagerRow.Cells.Count == 1) &&
(gridView.BottomPagerRow.Cells[0].Controls.Count == 1) &&
typeof(Table).IsAssignableFrom(gridView.BottomPagerRow.Cells[0].Controls[0].GetType()))
{
innerTable = gridView.BottomPagerRow.Cells[0].Controls[0] as Table;
}

if ((innerTable != null) && (innerTable.Rows.Count == 1))
{
if (gridView.PagerStyle != null)
{
className += gridView.PagerStyle.CssClass;
}
className = className.Trim();

writer.WriteLine();
writer.WriteBeginTag(tableHeaderFooter);
writer.WriteAttribute("class", className);
writer.Write(HtmlTextWriter.TagRightChar);
writer.Indent++;

TableRow row = innerTable.Rows[0];
foreach (TableCell cell in row.Cells)
{
foreach (Control ctrl in cell.Controls)
{
writer.WriteLine();
ctrl.RenderControl(writer);
}
}

writer.Indent--;
writer.WriteLine();
writer.WriteEndTag(tableHeaderFooter);
}
}
}
}

Listing 3 – Updated WritePagerSection

 

The gives us

The Default GridViewAdapter In Use With Pager Fixed

Figure 6 – The default GridViewAdapter in use with pager issue fixed

Adapting the GridView Adapter

If we look through the code of the GridView Adapter we will see lots of CSS classes being added these are mostly surplus to our need so we will remove them, also there are attributes being added that we don’t want such as those seen in Figure 6 section 1 has some attributes we do not want as we will control this via CSS and in 2 you can see one of the unwanted CSS classes being added, this style is for the caption which Bootstrap already styles.

Some Unwanted Code

Figure 7 – the creating the table tag

Note: The <table> cellpadding and cellspacing attributes are not supported in HTML5.
writer.WriteAttribute("cellpadding", "0");
writer.WriteAttribute("cellspacing", "0");

For block (2) since there is no Caption.CssClass on the GridView we cannot revert to just showing this if you feel the need for an specific class for the Caption element then feel free to add one by changing the appropriate line.

So the one thing that we can’t get for free as it were from Bootstrap on table formatting is the selected row see the Bootstrap Documentation so we need to make sure the SelectedRowStyle.CssClass is used for that se we need to look at the GetRowClass private method in the GridViewAdaptor see Listing 4 we need to make sure each property is only adding the specified CssClass. The main thing we do not need in Listing 4 is the “AspNet-GridView-xxxx” classes we only want the classes we specify explicitly to be used.

/// <summary>
/// Gets the row's CSS class.
/// </summary>
/// <param name="gridView">The grid view.</param>
/// <param name="row">The row.</param>
/// <returns>The CSS class.</returns>
/// <remarks>
/// Modified 10/31/2007 by SelArom to create CSS classes for all different row types and states.
/// </remarks>
private string GetRowClass(GridView gridView, GridViewRow row)
{
string className = row.CssClass;

switch (row.RowType)
{
case DataControlRowType.Header:
className += " AspNet-GridView-Header " + gridView.HeaderStyle.CssClass;
break;
case DataControlRowType.Footer:
className += " AspNet-GridView-Footer " + gridView.FooterStyle.CssClass;
break;
case DataControlRowType.EmptyDataRow:
className += " AspNet-GridView-Empty " + gridView.EmptyDataRowStyle.CssClass;
break;
case DataControlRowType.Separator:
className += " AspNet-GridView-Separator ";
break;
case DataControlRowType.Pager:
className += " AspNet-GridView-Pagination " + gridView.PagerStyle.CssClass;
break;
case DataControlRowType.DataRow:
switch (row.RowState)
{
case DataControlRowState.Normal:
className += " AspNet-GridView-Normal " + gridView.RowStyle.CssClass;
break;
case DataControlRowState.Alternate:
className += " AspNet-GridView-Alternate " + gridView.AlternatingRowStyle.CssClass;
break;
case DataControlRowState.Selected | DataControlRowState.Normal:
case DataControlRowState.Selected | DataControlRowState.Alternate:
className += " AspNet-GridView-Selected " + gridView.SelectedRowStyle.CssClass;
break;
case DataControlRowState.Edit | DataControlRowState.Normal:
case DataControlRowState.Edit | DataControlRowState.Alternate:
className += " AspNet-GridView-Edit " + gridView.EditRowStyle.CssClass;
break;
case DataControlRowState.Insert:
className += " AspNet-GridView-Insert ";
break;
}
break;
}

return className.Trim();
}

Listing 4 – GetRowClass private method

 

/// <summary>
/// Gets the row's CSS class.
/// </summary>
/// <param name="gridView">The grid view.</param>
/// <param name="row">The row.</param>
/// <returns>The CSS class.</returns>
/// <remarks>
/// Modified 2013-07-27 by Stephen J Naughton to use default CSS classes for each row type.
/// </remarks>
private string GetRowClass(GridView gridView, GridViewRow row)
{
string className = row.CssClass;

switch (row.RowType)
{
case DataControlRowType.Header:
className += gridView.HeaderStyle.CssClass;
break;
case DataControlRowType.Footer:
className += gridView.FooterStyle.CssClass;
break;
case DataControlRowType.EmptyDataRow:
className += gridView.EmptyDataRowStyle.CssClass;
break;
case DataControlRowType.Separator:
// we may want to indicate that there is a Separator here
className += " separator "; // need to check that this are not already in use in Bootstrap
break;
case DataControlRowType.Pager:
className += gridView.PagerStyle.CssClass;
break;
case DataControlRowType.DataRow:
switch (row.RowState)
{
case DataControlRowState.Normal:
className += gridView.RowStyle.CssClass;
break;
case DataControlRowState.Alternate:
className += gridView.AlternatingRowStyle.CssClass;
break;
case DataControlRowState.Selected | DataControlRowState.Normal:
case DataControlRowState.Selected | DataControlRowState.Alternate:
className += gridView.SelectedRowStyle.CssClass;
break;
case DataControlRowState.Edit | DataControlRowState.Normal:
case DataControlRowState.Edit | DataControlRowState.Alternate:
className += gridView.EditRowStyle.CssClass;
break;
case DataControlRowState.Insert:
// we may want to indicate that there is an insert here
className += " insert "; // need to check that this are not already in use in Bootstrap
break;
}
break;
}

return className.Trim();
}

Listing 5 – Updated GetRowClass

Listing 5 fixes up the GetRowClass so now we can specify our own CSS class for the parts of the grid we want to override.

Adding a Bootstrap Pager to the GridView Adapter

The last thing I want to do is add a Bootstrap compatible pager 

Bootstrap Pager

Figure 8 – Bootstrap Pager

I will continue this post with the fancy pager next time.

See my SkyDrive here for download details

Sunday, 21 July 2013

Bootstrap Friendly Dynamic Data

!Important: My first Blog Post since my heart attack Broken heart back on January 27th 2013 and then a triple heart bypass three weeks later, but I am back and feeling well Red heart.

Microsoft announced that Twitter-Bootstrap integration in the Build 2013 Day 2 Keynote (look at  15:00 index) this is cool (to be honest it had not been on my radar before this Embarrassed smile) but unless Microsoft produce new Dynamic Data Bootstrap templates we will need to roll them ourselves.

Note: I intend to integrate this and my own custom project templates into the new One ASP.Net thingy (at the moment it’s a bit of a Magic Unicorn until the extensibility is out for us to bolt onto).

BootstrapBootswatch

Figure – 1 Bootstrap formerly Twitter-Bootstrap and the free themes site Bootswatch.com

For a great introduction to Bootstrap visit Pluralsight.com and watch the Introduction to Bootstrap course.

So I tried to Bootstrapify (My new word In love) the standard Dynamic Data project Template and the first issue I came across is seen in Figure 2 which show the standard mark-up produced by ASP.Net Web Forms (and it’s worth noting that as of the June 2013 Visual Studios 2013 Preview we still have the same issue) and out of the box I cannot seem to find a way of turning the unhelpful mark-up off.

table-mark-up-Issues

Figure 2 – standard Table mark-up produced by .Net 4.5

So what is out next move well if you think back to the ASP.NET 2.0 CSS Friendly Control Adapters 1.0 (source here CSS Friendly Control Adapters) from ASP.Net 2.0 there is a way around this.

So step one was to download the source and get the project updated to VS2012 and fix up the GridView

Note: We are going to build our own Control adaptor as the we go but for now I just tested with the standard ASP.NET 2.0 CSS Friendly Control Adapters 1.0 to see what difference it made.

Add Bootstrap to the Dynamic Data Project

Add Bootstrap and jQuery (there appears to be no dependency on the Bootstrap package so you will need to add jQuery manually) to the Dynamic data application using NuGet 

add-bootstrap-using-nuget

Figure 3 – Add Bootstrap

Now we wire up the styles and scripts

In the templates supplied with Visual Studio 2012 you will note that the jQuery is wired up in the Global.asax.cs file (I am assuming Web Application Project here)

Note: I know we are no using JavaScript right now but we will later in the series so lets have it wired up now and be ready

jQuery

Figure 4 – jQuery 1.7.1

we need to update this to the latest version of jQuery (at the time of writing that was 2.0.3) see Figures 5 & 6 for the updates to Global.asax.cs and Site.master files.

jQuery-and-Bootstrap-scripts

Figure 5 – jQuery and Bootstrap scripts

bootstrap-added-to-site.master

Figure 6 – add the script reference to the Site.master file mark-up

Change the default CSS class for tables in out projects

css-styling

Figure 7 – Standard GridView mark-up

We will need to change all the CSS classes but for now we will simply replace the main CssClass on the GridView, change:

CssClass="DDGridView"

to

CssClass="table"

We will do a Replace in Files (Ctrl+Shift+H) gets us the dialog shown in Figure 8

replace-all-grid-view-css-classes

Figure 8 – Replace in files

Replace the default CSS style sheet with the Bootstrap style sheet

In the head of the Site.master file replace the default css style sheet with bootstrap’s style sheet. Make the changes shown between Figure 9 and  Figure 10

dynamic-data-default-style-sheet

Figure 9 – the Default Dynamic Data style sheet

bootstrap-style-sheets

Figure 10 – Bootstraps style sheets

Now we are ready to run the site and test the GridViews mark-up

As you can see if we view one of the data tables now we will see the issue in action Figure 11 the mark-up is not helping us here.

webforms-gridview-before-fixing-its-output-mark-up

Figure 11 – WebForms GridViewbefore fixing its output mark up

Adding the default ASP.NET 2.0 CSS Friendly Control Adapters to our project

Adding the default ASP.NET 2.0 CSS Friendly Control Adapters to our project you can just download the dll and the .browser files and add a reference to the DLL and copy the .browser file to the an App_Browsers folder.

add-app-browsers-folder

Figure 12 – Add a App_Browsers folder

add-reference-to-the-css-friendly-dll-and-add-a-copy-of-the-css-friendly-adapters-browser-file

Figure 13 – add a reference to the DLL and copy the CSSFriendlyAdapters.browser file to the App_Browsers  folder

gridview-with-the-mark-up-casihng-the-issue-removed-using-css-friendly-control-adapters

Figure 14 – GridView with the mark up causing the issue removed using CSS Friendly Control Adapters

finally-with-some-of-the-other-nice-table-classes

Figure 15 – Now with the following Bootstrap table classes table-bordered and table-striped added

Conclusion

In this series we will build our own Control Adapter specifically for Bootstrap to cover:

  • GridView
  • Menu
  • SiteMapPath
  • Login

We will also get the Edit and Details page templates working nicely with Bootstrap and of course all the FieldTemplates and Filters

Wednesday, 16 January 2013

Adding Multi Column Sort to Dynamic Data List Page

Well this is the first post of 2013 and has been brewing for a while, what we want is to be able to set the initial sort of the GridView on the List page or ListDetails page. Listing 1 shows what we want to achieve.

OrderDate sorted Descending and ShipName sorted Ascending

Figure 1 – OrderDate sorted Descending and ShipName sorted Ascending

The Multi Column Sort Attribute

Figure 2 shows the first version of the attribute, I was using magic strings. But in Figure 3 we have an enum from System.Web.UI.WebControls to help us out (I’m a bad speller and I tend to type too quickly) so I think that is better.

Early Version Of Metadata

Figure 2 – Early Version Of Metadata

Example Metadata

Figure 3 – Example Metadata

Listing 1 shows the MultiColumnSortAttribute I am using the params construct to get a list of values from the attribute and I am using the BuildColumnParametersDictionary method to convert the object array to a dictionary with a string key the column name and a value of the SortDirection.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MultiColumnSortAttribute : Attribute
{
private IDictionary<String, SortDirection> _columns;

public IDictionary<String, SortDirection> Columns
{
get { return this._columns; }
}

public MultiColumnSortAttribute(params object[] columnParameters)
{
_columns = BuildColumnParametersDictionary(columnParameters);
}

private IDictionary<String, SortDirection> BuildColumnParametersDictionary(object[] objArray)
{
var dictionary = new Dictionary<String, SortDirection>();
if ((objArray != null) && (objArray.Length != 0))
{
if ((objArray.Length % 2) != 0)
throw new InvalidOperationException(
String.Format("Need even number of control parameters.",
new object[0]));

for (int i = 0; i < objArray.Length; i += 2)
{
if (objArray[i] is String && objArray[i + 1] is SortDirection)
{
if (objArray[i] == null)
throw new InvalidOperationException(
String.Format("Column name [{0}] is null.", i));

// get column name from array
String columnName = objArray[i] as String;
if (columnName == null)
throw new InvalidOperationException(
String.Format("Column name '{0}' is not a String.",
objArray[i].ToString()));

// check for duplicate column names
if (dictionary.ContainsKey(columnName))
throw new InvalidOperationException(
String.Format("Column name '{0}' occurs more than once.",
columnName));

// get sort direction from array
if (objArray[i + 1] == null)
throw new InvalidOperationException(
String.Format("Sort direction '{0}' is not a of type SortDirection.",
objArray[i].ToString()));

SortDirection sortDirection = (SortDirection)objArray[i + 1];
dictionary[columnName] = sortDirection;
}
}
}
return dictionary;
}
}

Listing 1 - MultiColumnSortAttribute

An Extension Method to Add Multi Column Sort to the Page

This extension method in Listing 2 is a little more complicated than it would need to be to handle just the Multi Column Sort, it also handles the DisplayColumn attribute to, this way we cover all bases. 

Note: The GetAttribute<T>() extension method is covered here Writing Attributes and Extension Methods for Dynamic Data
/// <summary>
/// Sets the initial sort order.
/// </summary>
/// <param name="queryExtender">The query extender.</param>
/// <param name="table">The table.</param>
public static void SetInitialSortOrder(this QueryExtender queryExtender, MetaTable table)
{
var multiColumnSort = table.GetAttribute<MultiColumnSortAttribute>();
if (multiColumnSort != null)
{
var firstEntry = multiColumnSort.Columns.Keys.First();
var order = new OrderByExpression()
{
DataField = firstEntry,
Direction = multiColumnSort.Columns[firstEntry],
};

foreach (var item in multiColumnSort.Columns)
{
if (item.Key != firstEntry)
{
order.ThenByExpressions.Add(new ThenBy()
{
DataField = item.Key,
Direction = item.Value
});
}
}
queryExtender.Expressions.Add(order);
}
else if (table.SortColumn != null)
{
var order = new OrderByExpression()
{
DataField = table.SortColumn.Name,
Direction = table.SortDescending ? SortDirection.Descending : SortDirection.Ascending,
};
queryExtender.Expressions.Add(order);
}
}

Listing 2 – SetInitialSortOrder Extension Method

Using in the List Page Template

All we need to do in the List and ListDetails page templates is to add one line to the Page_Init method.

protected void Page_Init(object sender, EventArgs e)
{
table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
GridView1.SetMetaTable(table, table.GetColumnValuesFromRoute(Context));
GridDataSource.EntityTypeFilter = table.EntityType.Name;

// set initial sort order
GridQueryExtender.SetInitialSortOrder(table);
}

Listing 3 – adding the extension method to the Page Template

Download

You may need to look through the list of files on my sky drive to find the one you want, it’s called MultiColumnSort.zip

Thursday, 20 December 2012

Turning you ASP.Net Hyperlinks into images buttons using CSS and jQuery

First of all I’m no big jQuery or CSS geek I know a little and I search the net Smile

I was building a site for a client who wanted Image buttons not text links and you should know me if you have read past posts I believe Dynamic Data is about changing as little as possible in the pages I like to do it all if possible using the Metadata, so I thought is there a way of doing this using pure CSS, there is sort of but you are still left with the text of the hyperlink, that is where jQuery came in, and then I thought I need to add a CSS class to each button and again jQuery came to my aid. so here we go.

Note: I am using the latest Web Essentials by Mads Kristensen and using LESS for my style sheets, you are really missing out if you have VS2012 and are not using this experimental add-in for VS2012.

Note: If you don't have VS2012 the you can use Mindscape’s Web Workbench which offers SASS, LESS and CoffeeScript

So the first job is to somehow automatically add the CSS class to each of the buttons that we want to swap to image button.

function NAC_ReplaceHyperLinkWithImageButton() {
// array of button commands/names to affect
var toMatch = ["Details", "Edit", "Delete", "Update", "Cancel", "Insert", "Select", "New"];
// commands/names to replace
var toReplace = { "New": "Insert" };
$(document).ready(function () {
$("TABLE.DDDetailsTable a, TABLE.DDGridView a")
.each(function () {
// get the inner text
var innerText = $(this).text();
if ($.inArray(innerText, toMatch) > -1) {

// do replacement of commands to Replace
var found = toReplace[innerText];
// check there is a match in the lookup table
if (typeof found !== "undefined")
innerText = found;

// get the embedded text
$(this).addClass(innerText);

// add a tooltip
$(this).attr('Title', innerText);

// remove the hyperlinks text
$(this).text('');
}
});
});
}
// run script
NAC_ReplaceHyperLinkWithImageButton();
// bind script to AJAX bits
Sys.Application.add_load(NAC_ReplaceHyperLinkWithImageButton);

Listing 1 – jQuery for the swap out

So first of all we have an array this contains ALL the button names (the display text) that we want to affect, next we hook into the jQuery Read event then we are selecting command buttons List, Details, Edit and ListDetails pages using these two selectors:

"TABLE.DDDetailsTable a, TABLE.DDGridView a"

after that I am using the jQuery inArray function to check if the buttons text is one we want to affect. Then having gotten the innerText form the hyperlink we add the CSS class, then finally we remove the text so only the icon will show.

jQuery is so cool mixed with CSS.

ReplaceHyperlinkWithIcons

Figure 1- hyperlinks replace with buttons

Now people say LESS is more and in this case it is.

table.DDGridView a.Cancel,
table.DDDetailsTable a.Cancel
{
    width: 20px;
    height: 20px;
    display: inline-block;
    background-repeat: no-repeat;
    background-position: center center;
    background-image: url('../images/Cancel.png');
}

    table.DDGridView a.Cancel:hover,
    table.DDDetailsTable a.Cancel:hover
    {
        background-image: url('../images/Cancel-h.png');
    }

        table.DDGridView a.Cancel:hover:active,
        table.DDDetailsTable a.Cancel:hover:active
        {
            background-image: url('../images/Cancel-a.png');
        }

    table.DDGridView a.Cancel.aspNetDisabled,
    table.DDDetailsTable a.Cancel.aspNetDisabled,
    table.DDGridView a.Cancel.aspNetDisabled:hover,
    table.DDDetailsTable a.Cancel.aspNetDisabled:hover,
    table.DDGridView a.Cancel.aspNetDisabled:active,
    table.DDDetailsTable a.Cancel.aspNetDisabled:active
    {
        background-image: url('../images/Cancel-d.png');
    }

Listing 2 – the CSS

above in Listing 2 is the CSS for the Edit button as you can see we cover hyperlinks with a CSS class of “Edit” and we have three states normal hover and active that is when we click. Now for the LESS

/* ==== button mixin ==== */
.Button (@Name)
{
a.@{Name}
{
width: 20px;
height: 20px;
display: inline-block;
background-repeat: no-repeat;
background-position: center center;

background-image: url('../images/@{Name}.png');

&:hover
{
background-image: url('../images/@{Name}-h.png');

&:active
{
background-image: url('../images/@{Name}-a.png');
}
}
// link fix
&.aspNetDisabled,
&.aspNetDisabled:hover,
&.aspNetDisabled:active
{
background-image: url('../images/@{Name}-d.png');
}
// link fix
}
}

table.DDGridView,
table.DDDetailsTable
{
.Button(Cancel);
.Button(Delete);
.Button(Details);
.Button(Edit);
.Button(Update);
.Button(New);
.Button(Select);
.Button(Insert);
}

Listing 3 – LESS

In Listing 3 we have a MIXIN called Button and we pass in the name of the button we want, no repeating the same code and if you need to add an extra button it’s easy.

The only thing left is for me to add this to NuGet as a simple to apply package and you are away.

Updated: Package now available from NuGet here Replace ASP.Net Hyperlink With Image Button You will need to add a reference in you master page to the CSS and the JavaScript files which will be added to the Style and Scripts folder respectively.

Adding the NuGet package to an existing Dynamic Data site

Right Click the “References” node of the Web Application Project

Adding NuGet Package

Figure 1 – Adding NuGet Package to Project

Find the “Replace Hyperlink With Image Button” NuGet Package

Figure 2 – Find the “Replace Hyperlink With Image Button” NuGet Package

AddingScriptToHead

Figure 3 – Add the Style sheet and jQuery references to the head of the Master page 

Note: the highlighted sections, you will need to add a ~/ and ../ to the start od the CSS and Script links.

AddScriptToEndOfMasterPage

Figure 4 – Add a script tag just before the end of the BODY tag

Note: you can add your scripts and Style sheets your own way with bundling and minifying this is just an example.

Happy coding.

V1.0.4 now on NuGet you disabled icons if a link’s Enabled property is set to “false”;

Replace Hyperlink With Disabled Icons

Hopefully no more changes now.

Warning another change: v1.0.8 now has better icons and supports disabled buttons including removing the onclick event of the Delete button.

Friday, 14 December 2012

ASP.NET and Web Tools 2012.2 (Release Candidate)

 

Scott Hanselman announced ASP.NET and Web Tools 2012.2 (Release Candidate) today and it’s great for the first time it’s easy to deploy a DD Web Application Project (WAP) site precompiled and un-editable it’s just a check box now.

So install the new tools (get them from here Download Page and Release notes)

Note: If you already have publish settings then VS2012 will import them but they need to be in the root of your site.

New Publish Wizard

Figure 1 - New Publish Wizard

Navigate to the Settings Tab and check the “Precompile during publishing” checkbox and then click “Configure”

Disabled Editable markup.

Figure 2 - Disabled Editable markup.

un-check the “Allow precompiled site to be updatable” checkbox and your pages will no look like this Figure 3 if someone tries to edit them Smile

Precompiled aspx page no uneditable

Figure 3 - Precompiled aspx page no un-editable

And finally in VS2012 you can set the build configuration in each Publish profile, this works without having to change the build configuration in you project.

Warning: This sadly does not work for Dynamic Data yet it testes OK you already deployed to a site as it uses the place holders, I’m looking into it hopefully there will be a simple fix.

Thursday, 30 August 2012

O2 has major fail

O2 have just blocked external access to smtp.o2.co.uk this means you cannot send e-mail from your mobile if you are no connected to an O2 network and if you are abroad you will only be able to send email via your data account.

For me this is a major fail I havrelayeded on this from O2 for as long as I have been with them. I will be moving from O2 and finding a solution to sending e-mail that no ISP or Mobile provider will be able to block.

RIP O2

Steve



Well news is O2 don't seem to know what the issue is I just got told by first level support that it was not accessable from outside O2's network? but it is again I suspect I was being given a line...

Steve

Saturday, 26 May 2012

Creating a Tabbed Entity Template using Wijmo Open for Juice UI

Wijmo Open for Juice UI is quite cool and offers an alternative to using Ajax Control Toolkit which is still cool but Wijmo Open for Juice UI supports IE6+, Firefox 3+, Safari 3+, and Chrome browsers which should mean everything works.

I started customising Entity Templates here Custom Entity Templates – Dynamic Data 4 I decided to take this a little further and create a Tabbed UI for editing really long forms, I have done this with the Ajax Control Toolkit but thought I like to have a jQuery UI version which brings theming to the table.

Creating the Entity Template

Wijmo tabs in action

Figure 1 – Wijmo tabs in action

<asp:Panel ID="Panel1" runat="server">
    <ul>
        <li><a href="#tabs-1">Nunc tincidunt</a></li>
        <li><a href="#tabs-2">Proin dolor</a></li>
        <li><a href="#tabs-3">Aenean lacinia</a></li>
    </ul>
    <div id="tabs-1">
        <p>
            Proin elit arcu, rutrum commodo, vehicula tempus...
</p>
</div> <div id="tabs-2"> <p> Morbi tincidunt, dui sit amet facilisis feugiat...
</p> </div> <div id="tabs-3"> <p> Mauris eleifend est et turpis. Duis id erat...
</p> </div> </asp:Panel> <wijmo:WijTabs ID="Tabs1" runat="server" TargetControlID="Panel1"> </wijmo:WijTabs>

Listing 1 – mark-up to create the Wijmo tabs

As you can see there are two blocks the UL and the DIVs the UO is used to create the tabs and each DIV matched an LI element and is shown when the tab is selected.

This is not quite as easy to render in an Entity Template as was the AJAX tabs because we need to render two sets of elements two the page, first the UL then the DIVs and we also need the IDs of the DIVs for link in the LIs. so we have to first create the DIVs so we can get each ones client ID then create the UL and add the hyperlinks.

protected override void OnLoad(EventArgs e)
{
    var groupAttribute = Table.GetAttribute<GroupNamesAttribute>();
    if (groupAttribute == null)
        throw new InvalidOperationException("A GroupsAttribute is required for AJAX tab group to work.");

    var row = new HtmlTableRow();
    var td = new HtmlTableCell();
    row.Controls.Add(td);
    this.Controls.Add(row);
    // create tab container to hold each children column
    var panel = new Panel();
    panel.ID = "tabContainer_" + Table.Name;

    // add a tab panel for each children table
    var unorderedList = new HtmlGenericControl("ul");

    // SortedList of tabs
    var tabs = new SortedList<String, HtmlGenericControl>();

    // add DIV for each group
    foreach (var gi in groupAttribute.Groups)
    {
        var groupName = gi.Value;

        var tabDiv = new HtmlGenericControl("div");
        tabDiv.ClientIDMode = ClientIDMode.Static;
        tabDiv.ID = String.Format("{0}-{1}", Table.Name, groupName).Replace(" ", "-");

        // get columns for this group
        var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
                      where c.GetAttributeOrDefault<GroupAttribute>().Index == gi.Key
                      orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
                      select c;

        // add table for this tabs fields
        var htmlTable = new HtmlTable();
        htmlTable.Attributes.Add("class", "DDDetailsTable");
        htmlTable.CellPadding = 6;
        htmlTable.Attributes.Add("Name", groupName);

        // add fields
        foreach (MetaColumn column in columns)
        {
            // new row
            var htmlRow = new HtmlTableRow();
            htmlTable.Controls.Add(htmlRow);

            // add header cell
            var tdHeader = new HtmlTableCell();
            tdHeader.Attributes.Add("class", "DDLightHeader");
            tdHeader.InnerText = column.DisplayName;
            // add cell to row
            htmlRow.Controls.Add(tdHeader);

            // add data cell
            var tdData = new HtmlTableCell();
            // get field template
            var dynamicControl = new DynamicControl() 
                { 
                    Mode = Mode,
                    DataField = column.Name, 
                    ValidationGroup = this.ValidationGroup 
                };
            // add field template to cell
            tdData.Controls.Add(dynamicControl);
            // add cell to row
            htmlRow.Controls.Add(tdData);
        }

        // add the DynamicControl to the tab panel
        tabDiv.Controls.Add(htmlTable);

        // add the tab to list
        tabs.Add(groupName, tabDiv);
    }

    foreach (var gi in groupAttribute.Groups)
    {
        var groupName = gi.Value;

        // new 'LI'
        var tabLi = new HtmlGenericControl("li");

        // new hyperlink
        var hyperlink = new Literal();
        hyperlink.Text = String.Format("<a href=\"#{0}\" >{1}</a>", tabs[groupName].ClientID, groupName);

        // add hyperlink to 'LI'
        tabLi.Controls.Add(hyperlink);

        // add LIs items to UL
        unorderedList.Controls.Add(tabLi);
    }

    // add UL to panel
    panel.Controls.Add(unorderedList);

    // add DIVs to panel
    foreach (var gi in groupAttribute.Groups)
    {
        var groupName = gi.Value;
        panel.Controls.Add(tabs[groupName]);
    }

    // add the panel to the page
    td.Controls.Add(panel);

    // associate the panel with the Wijmo tabs
    var wijmoTabs1 = new WijTabs()
        {
            ID = "wijmoTabs1",
            TargetControlID = panel.ID
        };

    // add Wijmo tabs to the page
    td.Controls.Add(wijmoTabs1);
}

Listing 2 – the main code for the Entity Template

26-05-2012 12-13-50

Figure 2 – the finished Wijmo Tabs EntityTemplate

To setup you will need to have the Attributes and the Advanced Field Template Factory these are in the sample project. I will be putting on NuGet along with my other bits and pieces hopefully soon.

public static void RegisterRoutes(RouteCollection routes)
{
    // add new entity template factory that works with single files also
    DefaultModel.EntityTemplateFactory 
        = new AdvancedEntityTemplateFactory();

    DefaultModel.RegisterContext(typeof(Models.NorthwindEntities), 
        new ContextConfiguration() { ScaffoldAllTables = true });

    routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
    {
        Constraints = new RouteValueDictionary(new 
        { 
            action = "List|Details|Edit|Insert" 
        }),
        Model = DefaultModel
    });
}

Listing 3 – setting up the project Global.asax.cs

[MetadataTypeAttribute(typeof(Employee.EmployeeMetadata))]
[EntityUIHint("WijmoTabs")]
[GroupNames("Personal","Company","Address")]
public partial class Employee
{
    internal sealed class EmployeeMetadata
    {
        public int EmployeeID { get; set; }

        [Group(0)]
        [Display(Order = 0)]
        public string Title { get; set; }
        [Group(0)]
        [Display(Order = 1)]
        public string TitleOfCourtesy { get; set; }
        [Group(0)]
        [Display(Order = 2)]
        public string FirstName { get; set; }
        [Group(0)]
        [Display(Order = 3)]
        public string LastName { get; set; }
        [Group(0)]
        [Display(Order = 4)]
        public Nullable<DateTime> BirthDate { get; set; }
        [Group(0)]
        [Display(Order = 5)]
        public string Extension { get; set; }
        [Group(0)]
        [Display(Order = 6)]
        public byte[] Photo { get; set; }
        [Group(0)]
        [Display(Order = 7)]
        public string HomePhone { get; set; }

        [Group(1)]
        [Display(Order = 8)]
        public Nullable<DateTime> HireDate { get; set; }
        [Group(1)]
        [Display(Order = 9)]
        public Employee Manager { get; set; }
        [Group(1)]
        [Display(Order = 10)]
        public Nullable<int> ReportsTo { get; set; }
        [Group(1)]
        [Display(Order = 10)]
        public EntityCollection<Employee> Staff { get; set; }
        [Group(1)]
        [Display(Order = 11)]
        public EntityCollection<Order> Orders { get; set; }
        [Group(1)]
        [Display(Order = 12)]
        public EntityCollection<Territory> Territories { get; set; }
        [Group(1)]
        [Display(Order = 13)]
        public string Notes { get; set; }

        [Group(2)]
        [Display(Order = 14)]
        public string Address { get; set; }
        [Group(2)]
        [Display(Order = 15)]
        public string City { get; set; }
        [Group(2)]
        [Display(Order = 16)]
        public string PostalCode { get; set; }
        [Group(2)]
        [Display(Order = 17)]
        public string Region { get; set; }
        [Group(2)]
        [Display(Order = 18)]
        public string Country { get; set; }

        [Display(AutoGenerateField = false)]
        public string PhotoPath { get; set; }
    }
}

Listing 4 – Sample Metadata

You will also need NuGet and add the Wijmo Open for Juice UI to your project.

Get Wijmo Open for Juice UI from NuGet

Later I will add all the features that you get with jQuery UI such as tab sorting and alignment see the online Juice Explorer for all the features.

Just for good measure I have added the Wijmo Accordion to the project

Wijmo Accordian Entity Termplate

Figure 3 – Wijmo Accordion Entity Template

You can of course get even more by getting Studio for Studio for ASP.NET Wijmo from ComponentOne this is the best thing for ASP.Net I have seem come out for a while Open-mouthed smile as everyone else seems to be concentrating on either pure client or MVC Sad smile

Download from my sky drive WijmoTabs.zip