Showing posts with label DynamicData. Show all posts
Showing posts with label DynamicData. Show all posts

Wednesday, 21 August 2013

GridView Bootstrap Update

It would appear that issue that pushed me towards using Control Adapters to fix my issues with the GridView were “Not entirely accurate” it seems that if you do the following to the default List pages we will get the GridView to perform as we wish with Bootstrap. Scott Hunter has a post here Cleaning up Default GridView Mark-up this showed me we can clean the mark-up up fully without resorting to Control Adapters.

[画像:Default GridView Mark-Up For Dynamic Data]

Figure 1 – Default GridView Mark-Up For Dynamic Data

Going back to my original post Bootstrap Friendly Dynamic Data I showed the mark-up in Figure 2 and demonstrated this caused issues with Bootstrap and other CSS styles applied to the grid.

[画像:table mark-up Issues]

Figure 2- Table mark-up issues

So we change the mark-up to that in Figure 3 and then we get this nicely cleaned mark-up in Figure 4

[画像:Updated GridView Mark-Up For Dynamic Data]

Figure 3 – Updated GridView Mark-Up For Dynamic Data

[画像:table mark-up fixed]

Figure 4 – table mark-up fixed

GridView output with Bootstrap

Figure 5 – GridView output with Bootstrap

Hope that helps

Download

As usual you can get the code from the Project on Bootstrap Friendly Control Adaptors and on my SkyDrive

Sunday, 18 August 2013

Adding Validation Using Bootstrap Popover

I noticed an article by Damien Edwards called Updating the ASP.NET validator controls to change invalid control’s CSS class on non-JavaScript clients this got me thinking could I create an Control Adapter that would work for ALL Validators and would take advantage of the Bootstrap Tooltip or Popover JavaScript effects, Figure 1 is what I came up with.

[画像:Bootstrap Tooltip in action]

Figure 1- Bootstrap Popover in action

So how did I do it, well I’m not entirely sure this is good practice, I’m using a Control Adapter to get the the Pre-Render event and then I get the the control as a BaseValidator get access to the ControlToValidate the about 30 lines of code and we have the above for any Validator.

protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

var validator = Control as BaseValidator;
var controlType = Control.GetType().ToString();
var stringType = controlType.Substring(controlType.LastIndexOf(".") + 1, controlType.Length - controlType.LastIndexOf(".") - 1);

// build unique CCS class per validator type
var validatorError = String.Format("{0}-{1}", stringType, ERROR_CLASS);
var controlToValidate = Control.NamingContainer.FindControl(validator.ControlToValidate) as WebControl;
if (controlToValidate != null)
{
if (validator.IsValid)
{
// remove validator class
var className = String.Join(" ", controlToValidate.CssClass.Split(' ').Where(c => !c.Equals(validatorError)).ToArray());
controlToValidate.CssClass = className;
}
else
{
// add validator class
if (controlToValidate.CssClass.Split(' ').FirstOrDefault(c => c.Equals(validatorError)) == null)
controlToValidate.CssClass = String.IsNullOrEmpty(controlToValidate.CssClass)
? validatorError
: String.Format("{0} {1}", controlToValidate.CssClass, validatorError);

// add tooltip
controlToValidate.Attributes.Add("data-placement", "right");
controlToValidate.Attributes.Add("data-toggle", "popover");
controlToValidate.Attributes.Add("data-trigger", "hover");
controlToValidate.Attributes.Add("data-delay", "500");

// add title
controlToValidate.Attributes.Add("data-original-title", "Error!");
//TODO: add append errors to tooltip
controlToValidate.Attributes.Add("data-content", validator.ErrorMessage);

//$(document).ready(function () { $("#test").tooltip(); });
var datePickerScript = String.Format("$(document).ready(function () {{ $('#{0}').popover(); }});\n", controlToValidate.ClientID);
Control.Page.AddStartupClientScript(controlToValidate.ClientID, datePickerScript);
}
}
}

Listing 1 – Control Adapter OnPreRender

This piece of code does it all it finds the control we are validating and then add the CSS class or removes it depending on it being valid or not.

.inner-glow (@radius: 3px, @color: #f3ffa7)
{
-webkit-box-shadow: 0 0 @radius @radius @color inset !important;
box-shadow: 0 0 @radius @radius @color inset !important;
}

.required
{
.inner-glow;
}

.RequiredFieldValidator-validation-error,
.RegularExpressionValidator-validation-error,
.CompareValidator-validation-error,
.CustomValidator-validation-error,
.RangeValidator-validation-error,
.DynamicValidator-validation-error
{
.inner-glow(2px, fadeout(#ff0000, 50%));
border: 1px solid fadeout(#ff0000, 40%) !important;
}

Listing 2 – LESS Classes

I did the the styling using LESS as it’s available readily now with Mads Kristensen’s Web Essentials for Visual Studios 2012 and 2013.

Note: I think in the long term I want to do what Damien Edwards said inn his post and create an updated Validator for each of the main validators and also do a custom Popover that can be styles independently of the Bootstrap one.

Download

As usual you can get the code from the Project on Bootstrap Friendly Control Adaptors and on my SkyDrive

Sunday, 11 August 2013

Adding Some More Bootstrap Love to the Dynamic Data Project Template

In this article I want to add some of the nice features of Bootstrap to Dynamic Data

  • Bootstrap Navbar
  • Bootstrap Date Picker

Navbar

First of all we will move the list of table from the Default Page to the Site.master and have it as a menu in the nave bar.

[画像:Navbar]

Figure 1 – the Navbar

All we need for this is to move the Page_Load from the Default.ascx.cs page to the code behind of the Site.master.cs and then add the mark-up the Site.master see the two pages below:

<div class="navbar">
<div class="navbar-inner">
<a class="brand" href="#">Dynamic Data</a>
<ul class="nav">
<li>
<asp:LinkButton ID="LinkButton1" runat="server" PostBackUrl="~/Default.aspx">Home</asp:LinkButton>
</li>
<asp:ListView ID="Menu1" runat="server">
<LayoutTemplate>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Tables&nbsp;<b class="caret"></b></a>
<ul class="dropdown-menu">
<li runat="server" id="itemPlaceholder"></li>
</ul>
</li>
</LayoutTemplate>
<ItemTemplate>
<li runat="server">
<asp:DynamicHyperLink ID="HyperLink1" runat="server"><%# Eval("DisplayName") %></asp:DynamicHyperLink>
</li>
</ItemTemplate>
</asp:ListView>
</ul>
</div>
</div>

Listing 1 – the Menu mark-up

protected void Page_Load(object sender, EventArgs e)
{
System.Collections.IList visibleTables = Global.DefaultModel.VisibleTables;
if (visibleTables.Count == 0)
{
throw new InvalidOperationException("There are no accessible tables...");
}
Menu1.DataSource = visibleTables;
Menu1.DataBind();
}

Listing 2 – the Page_Load moved to Site.master

Date Picker

Add the Bootstrap Date Picker using NuGet

[画像:Bootstrap Date Picker]

Figure 2 – Bootstrap Date Picker

Then copy the DateTime and DateTime_Edit Field Templates and rename to Date and Date_Edit, then add a script and CSS references to the Site.master.

<div class="controls">
<div class="input-append date" id="DivDate" runat="server">
<div class="input-append">
<asp:TextBox
ID="TextBox1"
runat="server"
CssClass="input-small" />
<span class="add-on"><i class="icon-calendar"></i></span>
</div>
<asp:RequiredFieldValidator
runat="server"
ID="RequiredFieldValidator1"
CssClass=""
ControlToValidate="TextBox1"
Display="Static"
Enabled="false"/>
<asp:RegularExpressionValidator
runat="server"
ID="RegularExpressionValidator1"
CssClass=""
ControlToValidate="TextBox1"
Display="Static"
Enabled="false"/>
<asp:DynamicValidator
runat="server"
ID="DynamicValidator1"
CssClass=""
ControlToValidate="TextBox1"
Display="Static"/>
<asp:CustomValidator
runat="server"
ID="DateValidator"
CssClass=""
ControlToValidate="TextBox1"
Display="Static"
EnableClientScript="false"
Enabled="false"
OnServerValidate="DateValidator_ServerValidate"/>
</div>
</div>

Listing 3 – Date_Edit.ascs mark-up

// get default display format
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;

// create script
var datePickerScript = String.Format("$('#{0}').datepicker();", DivDate.ClientID);
Page.AddStartupClientScript(String.Format("{0}{1}BootstrapDatePicker", Table.Name, Column.Name), datePickerScript);

DivDate.Attributes.Add("data-date-format", dateFormat.ToLower());
TextBox1.Attributes.Add("placeholder", dateFormat);
TextBox1.ToolTip = Column.Description;

TextBox1.Columns = DateTime.Now.ToString(dateFormat).Length;

Listing 3 – Date_Edit.ascx.cs

Listings 2 & 3 show the necessary code to create the Date Field Templates

[画像:Bootstrap Date Picker in Action]

Figure 3 – Bootstrap Date Picker in Action

The Project is on Bootstrap Friendly Control Adaptors and on my SkyDrive

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

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.

Monday, 13 February 2012

Basic Auditing for Dynamic Data with Entity Framework 4.x

This is my first article of 2012 and I thought I had published an article on this previously but apparently not Crying face so I will now rectify that oversight.

The first this we need out audit fields, these are added to every entity we need to audit, next we need an Interface to allow us to fine entities with Audit fields.

AuditFields

Figure 1 – Audit Fields

public interface IAuditable
{
String CreatedByUserID { get; set; }
String CreatedDateTime { get; set; }
String UpdatedByUserID { get; set; }
String UpdatedDateTime { get; set; }
}

Listing 1 – IAuditable interface

We then need to add this to each entity that will be audited, this is just applied to you metadata classes on the Partial Class NOT the buddy class.

[MetadataType(typeof(AppAlterMetadata))]
public partial class AppAlter : IAuditable
{
internal class AppAlterMetadata
{
public Object ID { get; set; }

// other field deleted for brevety

// auditing fields
public Object CreatedByUserID { get; set; }
public Object CreatedDateTime { get; set; }
public Object UpdatedByUserID { get; set; }
public Object UpdatedDateTime { get; set; }
}
}

Listing 2 – the interface applied to each entity that requires auditing

Now for the code that does the auditing automatically,

public partial class MyEntities
{
/// <summary>
/// Called when [context created].
/// </summary>
partial void OnContextCreated()
{
// Register the handler for the SavingChanges event.
this.SavingChanges += new EventHandler(context_SavingChanges);
}


/// <summary>
/// Handles the SavingChanges event of the context control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private static void context_SavingChanges(object sender, EventArgs e)
{
var objects = ((ObjectContext)sender).ObjectStateManager;

// handle auditing
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Added));
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Modified), InsertMode: false);
}


/// <summary>
/// Auditing helper utility class
/// </summary>
internal static class AuditingHelperUtility
{
internal static void ProcessAuditFields(IEnumerable<Object> list, bool InsertMode = true)
{
foreach (ObjectStateEntry item in list)
{
var appUserID = GetUserId();
// deal with insert and update entities
var auditEntity = item.Entity as IAuditable;
if (auditEntity != null)
{

if (InsertMode)
{
auditEntity.CreatedByUserID = appUserID;
auditEntity.CreatedDateTime = DateTime.Now;
}

auditEntity.UpdatedByUserID = appUserID;
auditEntity.UpdatedDateTime = DateTime.Now;
}

}
}
}


public static String GetUserId()
{
return System.Web.HttpContext.Current.User.Identity.Name;
}
}

Listing 3 – the Audit code

Lets break this down into three sections

Section 1

Here we wire-up the SavingChanges handler in the OnContextCreated() partial method to do this we first need to create a partial class from out entities for you look in the EDMX code behind file you will see something like this;

EFClasses

Figure 2 – Entities classes

so we add a new class to the the project, make sure it has the same namespace as the EDMX code behind file (this is pretty much the same as for out metadata classes) and then we add the partial class same as the MyEntities (this will be the name you gave it when creating but it is there in the code behind you can’t miss it) class, see Listing 3.

The method is wired up with this line of code:

this.SavingChanges += new EventHandler(context_SavingChanges);

Section 2

Now in the context_SavingChanges method we simply get the ObjectStateManager  which has all the objects that are being added, updated and deleted, here we are only interested in the Added and Modified items. All we do is call our helper with each collection of objects.

Section 3

Looking at Listing 3 you will see the AuditingHelperUtility and it’s ProcessAuditFields method, here we first of all cast the each entity to the IAuditable interface and check for null if it isn't then set the appropriate properties and exit.

Finally

This can be expanded to cover many different requirements, I have maintained a separate audit table using this method with the addition of a little reflection.

Wednesday, 3 June 2009

A Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures

I just thought I’d mention this find ad David Ebbo mentioned overriding MetaTable to get at the table.GetScaffoldColumns() method, and then I found the Dynamic Data Futures sample inside of Preview 4 (which had a billing of being the Futures sample updated to work with Preview 4) and in side the it I found the CustomMetaClasses folder:

[画像:The CustomMetaClasses folder]

Figure 1 – The CustomMetaClasses folder

This of course give you the sample you need to do this your self my plan is to use this sample to make a MetaTable Class that I can pass in a Lambda function to do all my column generation without ever having to add IAutoFieldGenerator to the page (which in .Net 4.0 will not work on the Details, Edit and Insert pages as they are to be based on FormView to accomodate EntityTemplates).

public class Global : System.Web.HttpApplication
{
 private static MetaModel s_defaultModel = new CustomMetaModel();
 public static MetaModel DefaultModel
 {
 get
 {
 return s_defaultModel;
 }
 }
 public static void RegisterRoutes(RouteCollection routes)
 {
 DefaultModel.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
 {
 ScaffoldAllTables = true,
 MetadataProviderFactory = (type => new InMemoryMetadataTypeDescriptionProvider(type,
new AssociatedMetadataTypeTypeDescriptionProvider(type))) });

Listing 1 – Adding a DefaultModel based on CustomMetaModel from the CustomMetaClasses folder

Listing 1 shows the addition of the CustomMetaModel to the Global.asax allowing you to override the Meta Classes in one place this is cool.

Thanks again ASP.Net Team HappyWizard

Saturday, 2 August 2008

Dynamic Data and Field Templates - A Second Advanced FieldTemplate ***UPDATED***

  1. The Anatomy of a FieldTemplate.
  2. Your First FieldTemplate .
  3. An Advanced FieldTemplate .
  4. A Second Advanced FieldTemplate.
  5. An Advanced FieldTemplate with a GridView.
  6. An Advanced FieldTemplate with a DetailsView.
  7. An Advanced FieldTemplate with a GridView/DetailsView Project.

For this article we are going to convert the CascadingFilter from Dynamic Data Futures project this was thought of by Noimed in this thread.

Files Required for this Project

Here are all of the files we will need to copy to our project from the Dynamic Data Futures project:

From the sample website DynamicDataFuturesSample\DynamicData\Filters folder to the our projects DynamicData\FieldTemplates folder

  • Cascade.ascx
  • Cascade.ascx.cs

From the sample website DynamicDataFuturesSample root to our projects App_Code folder

  • CascadeAttribultes.cs

Plus we will need to add a reference to the DynamicDataFutures project or just copy the Microsoft.Web.DynamicData.dll to the bin folder of our project (you will need to create a bin folder manually if you just copy the dll).

What Cascading Filter does

Cascading filter in Edit/Insert modes, I pointed him to the previous post in this series and we eventually sorted it so it worked as a FieldTemplate. This returns the Primary Key of the parent table see Figure 1.

Order_Details relationships

Figure 1 - Order_Details relationships

In this diagram you can see that Product is grouped Category so the CascadingFilter user control would be ideal for picking the product on the Order_Detail Insert page.

Note: You can’t edit the Product on the Order_Detail form because the Primary Key of Order_Detail is OrderID combined with ProductID :D

So in our sample we will be filtering the Product by the Category.

Creating the Website Project and Adding the Files

The first thing to do will be to create a file based website and add the Northwind database to it. This can be done simply (if you have SQL Server Express 2005/2008 installed) by creating a App_Data folder and copying the Northwind.mdb file to it (the Northwind database can be downloaded from here).

Then add an App_Code folder to the website and add a new Linq to SQL classes item it call it NW.dbml and add at lease the above table to it.

Now copy the files listed in the “Files Required for this Project” and add the reference to Dynamic Data Futures project.

Lets add a reference to the Dynamic Data Futures project; I do this by first adding an existing project, you do this by clicking File->Add->Existing Project...

Adding an Existing project

Figure 2 - Adding an Existing project

Browse to the location you have you Dynamic Data Futures project and select the project file.

Now right click the website and choose Add Reference when the dialogue box pops up select the Projects tab and choose the Dynamic Data Futures project and click the OK button.

Your project should now look like Figure 3.

[画像:How the project should look after adding the files and references]

Figure 3 – How the project should look after adding the files and references

Note: Don’t forget the add your data context to the Global.asax file and set ScaffoldAllTables to true

Modifying the added files

Remove the namespace for the CascadeAttribute.cs file and save that’s done.

Note: Removing the namespace is for file based website only in a Web Application Project you would need to change the namespace to match your applications.

And now lets sort out the Cascade filter. We start by renaming the Cascade.ascx to Cascade_Edit.ascx and then edit both files:

<%@ Control 
 Language="C#" 
 AutoEventWireup="true" 
 CodeFile="Cascade_Edit.ascx.cs" 
 Inherits="Cascade_EditField" %>
<%-- Controls will be added dynamically. See code file. --%>

Listing 1 – Cascade_Edit.ascx

Remove the DynamicDataFuturesSample. from the beginning of the Inherits Control property.

Then edit the Cascade_Edit.ascx.cs file:

namespace DynamicDataFuturesSample
{
 public partial class Cascade_Filter : FilterUserControlBase, ISelectionChangedAware
 {

Listing 2 – Cascade_Edit.ascx.cs

Remove the namespace from around the control class and change the inheritance from FilterUserControlBase, ISelectionChangedAware to FieldTemplateUserControl as in Listing 3.

public partial class Cascade_Filter : FieldTemplateUserControl
{

Listing 3 – Altered Cascade_Edit.ascx.cs

Remove the following section as this is for Filters

public override string SelectedValue
{
 get
 {
 return filterDropDown.SelectedValue;
 }
}

Listing 4 – Remove SelectedValue method

Open ForeignKey_Edit.ascx.cs and copy the following sections to the Cascading_Edit.ascx.cs

protected override void ExtractValues(IOrderedDictionary dictionary)
{
 //...
} public override Control DataControl { //...
}

Listing 5 – Methods to copy from ForeignKey_Edit.ascx.cs

Now Edit the ExtractValues and DataControl methods so they look like Listing 6.

protected override void ExtractValues(IOrderedDictionary dictionary)
{
 // If it's an empty string, change it to null
 string val = filterDropDown.SelectedValue;
 if (val == String.Empty)
 val = null;
 ExtractForeignKey(dictionary, val);
}
public override Control DataControl
{
 get
 {
 return filterDropDown;
 }
}

Listing 6 – Finished ExtractValues and DataControl methods

Note: You will also probably need to add the following using: using System.Collections.Specialized; for the IOrderedDictionary and using System.Web.UI; for the Control.

Adding the Metadata

We have to add the following telling the Cascade FieldTemplate what it needs, it need to know what table to use to filter the main parent table by, in this case the Category table. And we need the UIHint to tell Dynamic Data to use the Cascade FieldTemplate.

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.Web.DynamicData;
[MetadataType(typeof(Order_DetailMD))]
public partial class Order_Detail
{
 public class Order_DetailMD
 {
 [Cascade("Category")]
 [UIHint("Cascade")]
 public object Product { get; set; }
 }
}
Listing 7 – the metadata

One last thing we need to add a Cascade.ascx FieldTemplate to the FieldTemplates folder as there is no way of Dynamic Data knowing what FieldTemplate to use in Read-Only mode. For this we will just copy ForeignKey.ascx as Cascade.ascx and change the class name from ForeignKeyField to CascadeField.

Add some Business Logic/Validation

Because we are using Order_Details table which has a composite primary key see below:

Order Details table

Figure 4 - Order Details table

So we need to add some business logic to validate this before insert.

public partial class NWDataContext
{
 partial void InsertOrder_Detail(Order_Detail instance)
 {
 var DC = new NWDataContext();
 var dk = DC.Order_Details.SingleOrDefault(
 od => od.OrderID == instance.OrderID && od.ProductID == instance.ProductID
 );
 if (dk != null)
 {
 // if a record is found throw an exception
 String error = "Duplicate Primary keys not allowed (OrderID={0} ProductID={1})";
 throw new ValidationException(String.Format(error, instance.OrderID, instance.ProductID));
 }
 else
 {
 // finnaly send to the database
 this.ExecuteDynamicInsert(instance);
 }
 }
}

Listing 8 – InsertOrder_Details partial method

This just checks the database to see if this is a duplicate primary key and if so generates a validation error.

Cascade FieldTemplate in Action 

Figure 5 – Cascade FieldTemplate in Action

Business Logic in action

Figure 6 – Business Logic in action

Adding Sorting to the Filters DropDownList ***UPDATED***

In the Cascase.ascx.cd FilterControl and Cascade_Edit.ascx.cs FieldTemplate you will find a method GetChildListFilteredByParent this returns the values for the filtered DropDownList, but as you will see this list is an unordered list. To add sorting to this list we need to add a Linq OrderBy clause. As you will see the code in Listing 9 is making use of the Expression class to create an expression tree smile_confused these are not really hard to understand, it’s just that there are so few examples and tutorials for us to get our teeth into.

So what I’ve done here is add a OrderBy clause which does the trick :D

private IQueryable GetChildListFilteredByParent(object selectedParent)
{
 var query = filterTable.GetQuery(context);
 // this make more sense as the parameter now has the table name (filteredTable.Name)
 // note the change from "product" to filterTable.Name
 var parameter = Expression.Parameter(filterTable.EntityType, filterTable.Name);
 // product.Category
 var property = Expression.Property(parameter, filterTableColumnName);
 // selectedCategory
 var constant = Expression.Constant(selectedParent);
 // product.Category == selectedCategory
 var predicate = Expression.Equal(property, constant);
 // product => product.Category == selectedCategory
 var lambda = Expression.Lambda(predicate, parameter);
 // Products.Where(product => product.Category == selectedCategory)
 var whereCall = Expression.Call(typeof(Queryable), "Where", new Type[] { filterTable.EntityType }, query.Expression, lambda);
 //================================== Order by ================================
 if (filterTable.SortColumn != null)
 {
 // this make more sense as the parameter now has the table name (filteredTable.Name)
 // table.sortColumn
 var sortProperty = Expression.Property(parameter, filterTable.SortColumn.Name);
 // Column => Column.SortColumn
 var orderByLambda = Expression.Lambda(sortProperty, parameter);
 //.OrderBy(Column => Column.SortColumn)
 MethodCallExpression orderByCall = Expression.Call(
 typeof(Queryable),
 "OrderBy",
 new Type[] { filterTable.EntityType, filterTable.SortColumn.ColumnType },
 whereCall,
 orderByLambda);
 //{
 //Table(Product).
 //Where(Products => (Products.Category = value(Category))).
 //OrderBy(Products => Products.ProductName)
 //}
 return query.Provider.CreateQuery(orderByCall);
 }//================================== Order by ================================
 else
 {
 return query.Provider.CreateQuery(whereCall);
 }
}

Listing 9 - GetChildListFilteredByParent

The section between the OrderBy comments is mine gleaned from various bits on the Internet, and also I’ve change the return line of the method to return the orderByCall which was whereCall previously.

To make this work you will need to add a DisplayColumn attribute to the metadata with the sort column added see Listing 10.

[MetadataType(typeof(ProductMD))]
[DisplayColumn("ProductName","ProductName")]
public partial class Product{}

Figure 10 – SortColumn added to DisplayColumn

The second parameter of DisplayColumn is the SortColumn when this is added then the GroupBy will be added to the where clause.

Note: You can transplant this code strait into the Cascade Filter as well smile_teeth.
Note: It should be possible to sort the parent DropDownList using a similar method.

And that about wraps it up.

Until next time.smile_teeth

Subscribe to: Comments (Atom)

AltStyle によって変換されたページ (->オリジナル) /