WP7 Client Certificates Part 2 (Client Certs on the Browser)

by jasonrshaver 28. September 2011 15:55

This post is part of a series on using client certificates in Windows Phone 7. I expect there to be 3 parts involved:

  1. Setting Up IIS Express
  2. Client Certificates on the Browser
  3. Client Certificates on the Emulator
  4. Client Certificates on the Phone

Setting Up IIS Express to Accept Client Certificates

First, lets tell IIS Express that we want to accept client certificates.  To do this, lets open up the IIS Express application host configuration file located at:

C:\Users\{Your User Name}\Documents\IISExpress\config\applicationhost.config

and as always, make a backup before modifying this file!  Make a notice of your site configuration located around line 161:

<site name="WebSecurity" id="2">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/" physicalPath="D:\Scratchpad\WebSecurity\WebSecurity" />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:5382:localhost" />
        <binding protocol="https" bindingInformation="*:44300:localhost" />
    </bindings>
</site>

Go to about line 330 and change the enabled attribute of the iisClientCertificateMappingAuthentication element to enabled:

<iisClientCertificateMappingAuthentication enabled="false">

And if you go to around line 314, replace the access element to the following block:

  <!-- If the user is using SSL and has a client certificate, use it -->
  <access sslFlags="SslNegotiateCert" />

  <!--Require SSL *AND* use a client certificate if there is one -->
  <!--<access sslFlags="Ssl, SslNegotiateCert" />-->

  <!--Require SSL *AND* require a client certificate -->
  <!--<access sslFlags="Ssl, SslRequireCert" />-->

You will see that there are 3 options in the above block.  For the sake of debugging, lets leave the first option, SslNegotiateCert, as the uncommented one.

Save the file, run your web application and you will now find that it asks you to select a client certificate.  In my case, working at Microsoft, I have lots:

image

Create a Client Certificate and Trust Chain

But, I don’t want to use a certificate from work, I want to create my own certificate.  To do that, we really need to create two certificates, a ‘localhost’ certificate to act as our client certificate, and a root certificate that we can place in our trusted root store. 

To do this, click Start and type ‘cmd’, right click on the ‘cmd.exe’ and select Run as Administrator.  Type “cd “\program files (x86)\Microsoft SDKs\Windows\v7.0A\bin” or wherever you may have a copy of makecert.exe installed.

First, lets create a new root certificate:

makecert -n "CN=localhost" -r -sv localhostCA.pvk localhostCA.cer

When you do this, you will be asked for a password to protect the private key.  You can choose none if you wish.  Now we need to use our new localhostCA certificate to issue a new client certificate:

makecert -pe -ss My -sr CurrentUser -a sha1 -sky exchange -n CN=localhost -sk SignedByLocalHostCA -ic localhostCA.cer -iv localhostCA.pvk

Now, before we can use that certificate, we need to ‘trust’ our LocalhostCA certificate.  Type the following into our command prompt:

start LocalhostCA.cer

and click Install Certificate:

image

And select “Place all certificates in the following store” and select Browse…

image

Click the Show physical stores checkbox and select Trusted Root Certification Authorities and Local Computer.

image

And than OK, Next, and Finish.  You should be greeted by a friendly “The import was successful.” dialog.

Now, lets go back to our web application created in Part 1 and hit F5.

image

Boom, now select the “localhost” certificate and you should be good to go.

Using the Client Certificate

So, now that we have a client certificate, how do we use it?  In our web project, go to Views\Home\Index.cshtml and add the following:

@{
    ViewBag.Title = "Home Page";
}

@if (Request.ClientCertificate.IsPresent == false)
{
<p>
    Client Certificate is not present.
</p>
}
else {
    <p>
    Client Certificate is found.<br /><br />

    User: <span>@User.Identity.Name</span> <br /><br />

    Certificate Details: <br />
    Issuer: <span>@Request.ClientCertificate["ISSUER"]</span><br />
    Subject: <span>@Request.ClientCertificate["SUBJECT"]</span><br />
    Serial Number: <span>@Request.ClientCertificate["SERIALNUMBER"]</span><br />
    Valid From: <span>@Request.ClientCertificate["VALIDFROM"]</span><br />
    Valid Till: <span>@Request.ClientCertificate["VALIDUNTIL"]</span><br />
    </p>        
}

F5 your application, select your client certificate and you should see something like the below:

image

Congratulations, in the next article, we will connect all this with a Windows Phone 7 project.

Tags: , , , ,

Blog

WP7 Client Certificates Part 1 (Setting Up IIS Express)

by JasonRShaver 28. September 2011 13:39

This post is part of a series on using client certificates in Windows Phone 7.  I expect there to be 3 parts involved:

  1. Setting Up IIS Express
  2. Client Certificates on the Browser
  3. Client Certificates on the Emulator
  4. Client Certificates on the Phone

Setting Up IIS Express

Create a new ASP.NET MVC 3 Web Application project

image

And select Internet Application with the default settings

image

Now to work with SSL, we need to use either IIS or IIS Express, and since I seem to make new applications every 15 minutes, I figure this is a good chance to get familiar with IIS Express.  The first step is to install IIS Express via the Web Platform Installer by following this link.  Once you are done clicking on a few accept/allow dialogs and clicking on the final Install button, wait for the install to finish and then restart Visual Studio 2010.

Right click on your web project and select “Use IIS Express”

image

and click Yes on the following dialog:

image

and note the address given at the next dialog:

image

Now to enable SSL, click on your project, pull up the Properties panel (F4) and set SSL Enabled to True.

image

And again, note the SSL URL which should be something like https://localhost:44300.  Lets set that as the default URL for our debugging.  Right click on the web project and select properties.  Go to the Web tab and update the Project Url under Use Local IIS Web server.

image 

Now, you should be able to F5 your project.  A security validation error will come up, if you click on the scary looking “Continue to this website (not recommended)”, you should see your website come up normally.

imageimage

Trusting Your Self Signed Certificate

So, now we are in the realm of security and certificates.  Lets start by telling our development machine to trust the SSL self-signed certificate that gets automatically created by IIS Express.  Click on Start, type ‘mmc’ and run the mmc.exe application.  Click File, and select Add/Remove Snap-In….  Double-click on Certificates and select Computer Account when the Certificates snap-in dialog pops up and click next.  On the next dialog, Local Computer should be selected and click Finish.

image

The resulting screen should look like this:

image

and after selecting OK, expand the Certificates (Local Computer) node on the left, and the Certificates node underneath that.  Find the localhost certificate issued by localhost.  Drag that certificate to the Trusted Root Certification Authorities node.  The result should look like this:

image

Now, lets go back to our web application and F5 our application again.  If everything went well, the web page should come right up without any errors and the address bar should no longer be a scary red:

image

Quick recap, where are we right now.  We have a web application, using SSL via IIS Express with a trusted self-signed certificate.  Let’s take this the next step, using self-signed client certificates.

Tags: , , , , ,

Blog

Upgrading Interprise 5.6.22 for Easy Skinning

by jasonrshaver 17. September 2011 23:06

So here is one of the issues I have with Interprise and ASP.Net Storefront:

image

The “Shopping Cart” header does not fit with the font, styling, color scheme, or really anything.  If I want to change it, I have to create a new graphic.  But not just one graphic, but 79 graphics:

image

So, what can we do about it?  Well here comes HTML + CSS to the rescue!

Changing your ASPX file

The default box for the Shopping Cart example above looks like this:

  1. <table width="100%" cellpadding="2" cellspacing="0" border="0" style="border-style: solid;
  2.     border-width: 0px; border-color: #444444">
  3.     <tr>
  4.         <td align="left" valign="top">
  5.             <asp:Image ID="ShoppingCartGif" AlternateText="" runat="server" ImageAlign="AbsMiddle" /><br />
  6.             <table width="100%" cellpadding="4" cellspacing="0" border="0" style="border-style: solid;
  7.                 border-width: 1px; border-color: #444444;">
  8.                 <tr>
  9.                     <td align="left" valign="top">
  10.                         <asp:Literal ID="CartItems" runat="server"></asp:Literal>
  11.                     </td>
  12.                 </tr>
  13.             </table>
  14.         </td>
  15.     </tr>
  16. </table>

The asp:Image is where the Shopping Cart displays the blue boxes we wish to get rid of.  Lets add after that asp:Image control with the following:

  1. <h3 class="boxHeader"><span>Shopping Cart</span></h3>

and set the asp:Image control to have a Visible=”false” attribute like this:

  1. <asp:Image ID="ShoppingCartGif" AlternateText="" runat="server" ImageAlign="AbsMiddle" Visible="false" />

and change the table beneath all that to have no style, but instead use the “boxBody” class, like this:

  1. <table width="100%" cellpadding="4" cellspacing="0" border="0" class="boxBody">

So the completed block looks like this:

  1. <table width="100%" cellpadding="2" cellspacing="0" border="0" style="border-style: solid;
  2.     border-width: 0px; border-color: #444444">
  3.     <tr>
  4.         <td align="left" valign="top">
  5.             <asp:Image ID="ShoppingCartGif" AlternateText="" runat="server" ImageAlign="AbsMiddle" Visible="false" />
  6.             <h3 class="boxHeader"><span>Shopping Cart</span></h3><br />
  7.             <table width="100%" cellpadding="4" cellspacing="0" border="0" class="boxBody">
  8.                 <tr>
  9.                     <td align="left" valign="top">
  10.                         <asp:Literal ID="CartItems" runat="server"></asp:Literal>
  11.                     </td>
  12.                 </tr>
  13.             </table>
  14.         </td>
  15.     </tr>
  16. </table>

One thing to note about the above, we could have deleted the asp:Image control, but made it invisible instead.  If we deleted that line, we would have to change the ShoppingCart.cs file to no longer look up the image and I am a bit too lazy for that.

Getting Some CSS Up In Here

So, now what?  Lets create some CSS that can make everything pretty:

  1. h3.boxHeader
  2. {
  3.     color: #AAA;
  4.     margin: 0px 10px;
  5.     display:inline;
  6.     float: left;  
  7.     background: transparent url('images/boxHeader.png') no-repeat top right;    
  8.     font: bold 12px/22px Verdana, Arial, Helvetica, Sans-serif;
  9. }
  10. h3.boxHeader span
  11. {
  12.     margin: 0px 15px -4px -10px;
  13.     padding: 1px 20px 5px 18px;
  14.     position: relative;
  15.     float: left;
  16.     background: transparent url('images/boxHeader.png') no-repeat top left;  
  17. }
  18. table.boxBody
  19. {
  20.     border-style: solid;
  21.     border-width: 2px;
  22.     border-color: #AAA;
  23. }

What this does, is take our boxHeader.png image, which looks something like this:

image

or

image

What I like about the first image is that I can use the table.boxBody CSS style to blend in and get a nice unified look:

image

Pretty cool huh?

And Repeat

So, once you have the above done as a proof of concept, you need to start changing this all over the place.  Here is a list of files that need to updated to use my H3/Span method instead of the image.  ** Make sure you make backups of each file you edit! **

  • ShoppingCart.aspx
  • Account.aspx
  • CreateAccount.aspx
  • Xml_Packages\page.search.xml.config
  • CheckOutShipping.aspx.cs (* see note)
  • CheckOutPayment.aspx.cs (* see note)
  • CheckOutReview.aspx.cs (* see note)
  • [ I am sure I missed some ]

For the XML Packages, instead of changing the boxes, you can modify the BoxFrameStyle AppConfig, but I prefer to use a class instead so I can do a CSS only skin.

About CheckOut[blah].aspx.cs Files

Update the DisplayOrderSummary method like so:

  1. private void DisplayOrderSummary()
  2. {
  3.     //OrderSummary.Text = _cart.RenderHTMLLiteral(new CheckOutShippingPageLiteralRenderer());
  4.     OrderSummary.Text = _cart.RenderHTMLLiteral(new CheckOutShippingPageLiteralRenderer()).Replace("<img src=\"skins/Skin_1/images/orderinfo.gif\" align=\"absbottom\" border=\"0\">", "<h3 class=\"boxHeader\"><span>Order Summary</span></h3>");
  5. }

Each file requires slight difference, but you should be able to figure them out.

No clue why they bake the graphic/skin into the code like that.  You can Modify the type, it exists in the InterpriseSuiteEcommerceCommon.InterpriseIntegration namespace, but there is a large amount of code in there. 

Tags: , , ,

Blog

How to Have an Awesome Home Page

by jasonrshaver 14. September 2011 22:23

So, you want to make a good impression on your customers?  Take a critical look at your website and think, “does my front page look like I want it?”  Likely you make some compromises because your template requires it.  This is all about how to get past that limit and do more with your site!

(Picture still coming)

Allowing Special Default Template

In the 800 eCommerce module, under Setup –> Application Configuration, do a search for HomeTemplate.  Set the Config Value to ‘hometemplate.ascx’.

image

Before You Start

This modification requires the modification, Helpers for Adding Server Side Controls In Interprise 5.6.22

Creating a HomeTemplate

This is something that you can go crazy about, or go really simple.  The simple way, take your template.ascx file, make a copy, and rename it ‘hometemplate.ascx’.  Here are some common things you would want to do to this page:

  • Remove any redundant menus.  Some sites have both a menu on the top and a menu on the left.  Consider removing one of them. 
  • Refine your 'skin’ by hand to improve its layout.  This is the one page where you can safely do things on a by-pixel basis.
  • Remove as much distracting content as possible, make it really easy for your visitors to find what the next step is.
  • Start telling a ‘story’ that starts with your home page and ends with your customers placing an order.

Once you get your HomeTemplate clean of clutter, we need to remove the ‘automatic’ content from it.  Look for these lines:

  1. <!-- CONTENTS START -->
  2. <asp:PlaceHolder ID="PageContent" runat="server"></asp:PlaceHolder>
  3. <!-- CONTENTS END -->

Delete them. 

Promotional Image Rotator

So, now what can we do here?  Let’s create an awesome image rotator!  Now, I use Telerik controls myself, but I am sure a quick Google search can help you find something free if that’s more your style.  I tend to be willing to pay for a product that is supported by such an awesome company.

To use some DataBound controls that use the “System.Web.UI.DataboundLiteralControl”, we have to make a very minor change to the default App_Code\SkinBase.cs file.  Find the ProcessControl method and replace it with this one:

  1. private void ProcessControl(Control ctl, bool includeChildren)
  2. {
  3.     IEditableTextControl e = ctl as IEditableTextControl;
  4.     if (e != null) e.Text = ReplaceTokens(e.Text);
  5.     else
  6.     {
  7.         ITextControl t = ctl as ITextControl;
  8.         if (t != null &&
  9.             t.GetType() != typeof(System.Web.UI.DataBoundLiteralControl))
  10.         {
  11.             t.Text = ReplaceTokens(t.Text);
  12.         }
  13.     }
  14.     IValidator v = ctl as IValidator;
  15.     if (v != null)
  16.     {
  17.         v.ErrorMessage = ReplaceTokens(v.ErrorMessage);
  18.     }
  19.     if (includeChildren && ctl.HasControls())
  20.     {
  21.         IterateControls(ctl.Controls);
  22.     }
  23. }

The only difference is that we ignore DataBound types (or at least the one that causes errors).  Now that the boring stuff is out of the way.  Lets add the control to our hometemplate.ascx:

  1.                     <div>
  2.     <telerik:RadRotator ID="Rotator1" runat="Server" Width="750" Height="300"
  3.                         ItemHeight="300" ItemWidth="750" ScrollDirection="Down"
  4.                         DataSourceID="xmlDataSource1" ScrollDuration="1500"
  5.                         FrameDuration="3500" >
  6.         <ItemTemplate>
  7.             <div class="apiContainer">
  8.                 <div class="apiContent">
  9.                     <a href="<%# XPath("Link/@Url") %>">
  10.                         <img src="<%# XPath("Image/@Url") %>"
  11.                              alt="<%# XPath("Image/@Alt") %>" />
  12.                     </a>
  13.                 </div>
  14.             </div>
  15.         </ItemTemplate>
  16.     </telerik:RadRotator>
  17.     <asp:XmlDataSource ID="xmlDataSource1" runat="Server" DataFile="images/ttx/Promo.xml">
  18.     </asp:XmlDataSource>
  19. </div>

And add the following to the SetupControlsRequiringScriptManager in App_Code\SkinBase.cs (added in the Begin You Begin step) right after the comment:

  1. // script.Append(SetupServerControl(frm.FindControl("MyControlId") as WebControl));
  2. script.Append(SetupServerControl(frm.FindControl("Rotator1") as WebControl, "            var oRotator = $find(\"Rotator1\")\n            oRotator.repaint();\n"));

Getting Data Into Your Rotator

The RadRotator control’s data loading is handled by the asp:XmlDataSource control.  As pasted above, the application will load a file located at /images/ttx/Promo.xml, you can change this path to something that makes sense for you, but here is the file format you should use:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <root>
  3.     <PromoItem>
  4.         <Title></Title>
  5.         <Image Url="/images/ttx/FrontAdXiomInStock.jpg"
  6.                alt="XIOM in stock"/>
  7.         <Link Url="/t-NewsXiomInStock.aspx" />
  8.     </PromoItem>
  9.     <PromoItem>
  10.         <Title></Title>
  11.         <Image Url="/images/ttx/GalaxyRubberComparisons.png"/>
  12.         <Link Url="/t-NewsXiomInStock.aspx" />
  13.     </PromoItem>
  14. </root>

For each image/add you want to use, create a new PromoItem with the elements/attributes shown.  Remember to use alt tags on your images.  It is better for your search results AND in some cases, it is the law.

For the Link Url, you can create topics, one for each promotion.  You could also just link to a product, category, manufacturer, or whatever you want.

Now, if you run your site, you will notice you have an awesome rotating promotion in your site!

Loading Promo Data From a Topic

But wait, there’s more!  How about using a topic instead of an XML file you say?  Easy.  Change the asp:XmlDataSource to look like this:"

  1. <asp:XmlDataSource ID="xmlDataSource1" runat="Server" >
  2.     <Data>
  3.     (!Topic Name="PromoData"!)
  4.     </Data>
  5. </asp:XmlDataSource>

And then add a topic to your website that looks something like this:

(Click to enlarge)

image

More in the upcoming Part 2!

Tags:

Blog

Helpers for Adding Server Side Controls

by jasonrshaver 14. September 2011 21:39

If you are trying to integrate an ASP.NET server side control, especially one that requires use of a ScriptManager, you will find that the lack of a server side form element can put an end to your ambition.  Getting around this is not that hard, here are the steps.

Changes to SkinBase

Add the following methods to your App_Code\SkinBase.cs file:

  1. #region Setup Controls Requiring ScriptManager
  2. private void SetupControlsRequiringScriptManager()
  3. {
  4.     HtmlForm frm = this.ThisForm;
  5.     if (frm == null)
  6.         return;
  7.  
  8.     string randomId = Guid.NewGuid().ToString("N").Substring(0, 5);
  9.     StringBuilder script = new StringBuilder();
  10.     script.AppendFormat("<script type=\"text/javascript\" language=\"Javascript\">\n");
  11.     script.AppendFormat("    function loadServerControl_{0}() {{\n", randomId);
  12.  
  13.     // script.Append(SetupServerControl(frm.FindControl("MyControlId") as WebControl));
  14.  
  15.     script.AppendFormat("    }}\n");
  16.     script.AppendFormat("    $add_windowLoad(loadServerControl_{0});\n", randomId);
  17.     script.AppendFormat("</script>\n");
  18.  
  19.     Page.ClientScript.RegisterStartupScript(this.GetType(), randomId, script.ToString());
  20.  
  21. }
  22. private String SetupServerControl(WebControl control)
  23. {
  24.     return SetupServerControl(control, String.Empty);
  25. }
  26. private String SetupServerControl(WebControl control, String customScript)
  27. {
  28.     if (control == null)
  29.         return string.Empty;
  30.  
  31.     HtmlForm frm = this.ThisForm;
  32.             
  33.     string MyControlId = control.ID;
  34.     string MyContainerId = MyControlId + "_Panel";
  35.     int MyOriginalIndex = FindParentIndex(control);
  36.     var MyPlaceHolder = new Panel();
  37.     MyPlaceHolder.ID = MyContainerId;
  38.  
  39.     Control MyParentControl = control.Parent;
  40.     MyParentControl.Controls.Remove(control);
  41.     MyParentControl.Controls.AddAt(MyOriginalIndex, MyPlaceHolder);
  42.     frm.Controls.Add(control);
  43.     control.Style["display"] = "none";
  44.  
  45.     StringBuilder script = new StringBuilder();
  46.     script.AppendFormat("        var control_{0} = document.getElementById('{0}');\n", MyControlId);
  47.     script.AppendFormat("        var panel_{0} = document.getElementById('{0}');\n", MyContainerId);
  48.     script.AppendFormat("        if(control_{0} && panel_{1}) {{\n", MyControlId, MyContainerId);
  49.     script.AppendFormat("            panel_{1}.appendChild(control_{0});\n", MyControlId, MyContainerId);
  50.     script.AppendFormat("            control_{0}.style.display = '';\n", MyControlId, MyContainerId);
  51.  
  52.     if (String.IsNullOrEmpty(customScript) == false)
  53.         script.AppendFormat(customScript, MyControlId, MyContainerId);
  54.  
  55.     script.AppendFormat("        }}\n");
  56.     return script.ToString();
  57. }
  58. #endregion

And then add the SetupControlsRequiringScriptManager(); to your OnPreRender method:

  1. protected override void OnPreRender(EventArgs e)
  2. {
  3.     if (HttpContext.Current != null)
  4.     {
  5.         //register the ScriptManager before loading controls or the ComponentArt menu won't work with AJAX pages
  6.         CheckIfRequireScriptManager();
  7.  
  8.         if (!this.HasControls()) //Probably a web form so use the control management technique
  9.         {
  10.             if (m_Template.Content != null)
  11.             {
  12.                 //No controls so html must come from RenderContents. Create a literal to contain RenderContents
  13.                 m_Template.Content.Controls.Add(ParseControl(CreateContent()));
  14.             }
  15.             this.Controls.Clear();
  16.             this.Controls.Add(m_Template);
  17.             // Now move the template child controls up to the page level so the ViewState will load
  18.             while (m_Template.Controls.Count > 0)
  19.             {
  20.                 this.Controls.Add(m_Template.Controls[0]);
  21.             }
  22.         }
  23.  
  24.         SetupControlsRequiringScriptManager();
  25.         SetupMenu();                
  26.     }
  27.  
  28.     base.OnPreRender(e);
  29. }

Using The Result

Now, in your template.ascx, you can take your server side control and give it an ID, for example:

  1. <telerik:RadRotator ID="Rotator1" runat="Server" Width="750" Height="300"
  2.                     ItemHeight="300" ItemWidth="750" ScrollDirection="Down"
  3.                     DataSourceID="xmlDataSource1" ScrollDuration="1500"
  4.                     FrameDuration="3500" >

And add the following line to the SetupControlsRequiringScriptManager method right under the example comment:

  1. // script.Append(SetupServerControl(frm.FindControl("MyControlId") as WebControl));
  2. script.Append(SetupServerControl(frm.FindControl("Rotator1") as WebControl));

and you are done!

How Does It Work

This method works the same way as the ASP.NET Menu control workaround that is built into App_Code\SkinBase.cs, it creates a placeholder for the control and places the control into the HtmlForm, and tells it to not display (via the CSS display tag). Then, when the page is loaded into a browser, a script takes the control and puts it into the placeholder and sets its to display. 

** Warning ** Webkit browsers, such as Chrome and Safari, do not handle the visibility changes of div elements the same as other browsers.  If you are having trouble getting your controls to display in Webkit browsers when everything else is working fine, contact your control vendor for a workaround. 

Tags:

Blog

Improving Interprise 5.6.22’s Web Search Page

by jasonrshaver 10. September 2011 00:09

So, lets take a look at the Interprise search page.  You know, the one that looks something like this:

image

Don’t get me wrong, It functions, and I find that I use this screen all the time over the course of my work day, but I just wish it could answer some questions for me, such as these:

  • How much does this product cost again?
  • What do I have in stock right now?
  • What was the name of that discontinued product again?

Template Changes

So, first, we need to make one change to our template to support this.  Don’t fret, it is really easy:

  1. <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.3.min.js" />

Put the above line to your skin’s template.ascx’s head section.  If you don’t know jQuery yet, it is the #1 tool needed to make your site pop.  It does not do it for free, but between XmlPackages, SQL, and jQuery, there is almost nothing you can’t do. 

Including Discontinued Products In Search Results

Ok, this one is really easy!  Create the following SQL Stored Procedure which is just slightly modified from the default aspdnsf_WebSearchInventory procedure:

  1.  
  2.               CREATE PROCEDURE [dbo].[ttx_WebSearchInventoryExt]
  3. @SearchTerm            NVARCHAR(3000),              
  4. @WebSiteCode            NVARCHAR(30),              
  5. @LanguageCode            NVARCHAR(50),            
  6. @ItemCode                NVARCHAR(1000) = NULL,             
  7. @ItemType                NVARCHAR(50) = NULL,          
  8. @UnitMeasure            NVARCHAR(30) = NULL,          
  9. @CategoryID            NVARCHAR (100) = NULL,
  10. @SectionID                NVARCHAR (100) = NULL,
  11. @ManufacturerID        NVARCHAR (100) = NULL,
  12. @SearchDescriptions    VARCHAR(5),    
  13. @CurrentDate            DATETIME,
  14. @ShowDiscontinued        BIT = 0
  15.     
  16.               AS
  17.               BEGIN
  18.               SET NOCOUNT ON
  19.           
  20. SET @SearchTerm = Replace(@SearchTerm,'''','''''')          
  21. SET @SearchTerm = '''%' + @SearchTerm + '%'''              
  22. SET @SearchTerm = Replace(@SearchTerm,'+',' ')          
  23. SET @ItemCode = NULLIF(@ItemCode, NULL)           
  24.           
  25. IF (@ItemType = 'ANY')           
  26.   SET @ItemType = '''%'''             
  27. ELSE          
  28.   SET @ItemType = '''%' + @ItemType + '%'''            
  29.           
  30. IF (@UnitMeasure = 'ANY')           
  31.   SET @UnitMeasure = '''%'''              
  32. ELSE          
  33.   SET @UnitMeasure = '%' + @UnitMeasure + '%'''             
  34.               
  35. DECLARE @SearchDescriptionString VARCHAR(1000)          
  36.   IF (@SearchDescriptions = '0')           
  37.   SET @SearchDescriptionString = ' '           
  38.   ELSE          
  39.   SET @SearchDescriptionString = ' OR IIWOD.WebDescription LIKE ' + @SearchTerm           
  40.       
  41. DECLARE @JoinUnitMeasureString VARCHAR(1000)          
  42.   IF (@UnitMeasure = '''%''')           
  43.   SET @JoinUnitMeasureString = ' '           
  44.   ELSE          
  45.   SET @JoinUnitMeasureString = ' INNER JOIN InventoryUnitMeasure IUM WITH (NOLOCK) ON IUM.ItemCode = A.ItemCode AND ISNULL(UnitMeasureQty,0)>0 AND IUM.UnitMeasureCode LIKE ''' + @UnitMeasure + ''             
  46. DECLARE @EntityBuilderString VARCHAR(1000)
  47. SET @EntityBuilderString = ''
  48.   IF (@CategoryID <> '0')
  49.     BEGIN
  50.         SET @EntityBuilderString = 'INNER JOIN (SELECT ItemCode FROM InventoryCategory A WITH (NOLOCK) INNER JOIN SystemCategory B WITH (NOLOCK) ON A.CategoryCode = B.CategoryCode WHERE B.Counter = ''' + @CategoryID +
  51.                                    ''') IC ON IC.ItemCode = A.ItemCode'
  52.     END
  53.   IF (@SectionID <> '0')
  54.     BEGIN
  55.         SET @EntityBuilderString = @EntityBuilderString + ' INNER JOIN (SELECT ItemCode FROM InventoryItemDepartment A WITH (NOLOCK) INNER JOIN InventorySellingDepartment B WITH (NOLOCK) ON A.DepartmentCode = B.DepartmentCode WHERE B.Counter = ''' + @SectionID +
  56.                                    ''') IIDep ON IIDep.ItemCode = A.ItemCode'
  57.     END
  58.   IF (@ManufacturerID <> '0')
  59.     BEGIN
  60.         SET @EntityBuilderString = @EntityBuilderString + ' INNER JOIN (SELECT A.ManufacturerCode, A.ItemCode FROM InventoryItem A WITH (NOLOCK) INNER JOIN SystemManufacturer B WITH (NOLOCK) ON A.ManufacturerCode = B.ManufacturerCode WHERE B.Counter = ''' + @ManufacturerID +
  61.                                    ''') SM ON SM.ItemCode = A.ItemCode'
  62.     END
  63. DECLARE @SQL VARCHAR(8000)       
  64. SET @SQL='SELECT          
  65.     A.*, IID.ItemDescription, IIWOD.WebDescription, H.Status, H.RetailPrice, H.UnitsInStock FROM InventoryItem A WITH (NOLOCK)          
  66.     LEFT JOIN (SELECT ItemCode FROM InventoryItemPricingDetail B WITH (NOLOCK)      
  67. INNER JOIN SystemCurrency C WITH (NOLOCK) ON B.CurrencyCode = C.CurrencyCode WHERE IsHomeCurrency = 1) D ON A.ItemCode = D.ItemCode   
  68. LEFT JOIN (SELECT TOP 1 ItemKitCode FROM InventoryKitPricingDetail E WITH (NOLOCK)             
  69. INNER JOIN SystemCurrency F WITH (NOLOCK) ON E.CurrencyCode = F.CurrencyCode WHERE IsHomeCurrency = 1) G ON A.ItemCode = G.ItemKitCode    
  70.   LEFT JOIN (
  71.  
  72.                         SELECT iiapv.ItemCode
  73.                             , iiapv.Status
  74.                             , iiapv.RetailPrice
  75.                             , SUM(istv.UnitsInStock) as UnitsInStock
  76.                           FROM [InventoryItemAndPricingView] iiapv WITH (NOLOCK)
  77.                           JOIN InventoryMatrixItem imi WITH (NOLOCK) ON iiapv.ItemCode = imi.ItemCode
  78.                           JOIN InventoryStockTotalView istv WITH (NOLOCK) ON imi.MatrixItemCode = istv.ItemCode
  79.                         WHERE iiapv.LanguageCode = ''' + @LanguageCode + '''
  80.                            AND istv.LanguageCode = ''' + @LanguageCode + '''
  81.                            AND iiapv.ItemType = ''Matrix Group''
  82.                       GROUP BY iiapv.ItemCode
  83.                             , iiapv.Status
  84.                             , iiapv.RetailPrice
  85.                 UNION
  86.                         SELECT iiapv.ItemCode
  87.                             , iiapv.Status
  88.                             , iiapv.RetailPrice
  89.                             , istv.UnitsInStock as UnitsInStock
  90.                           FROM [InventoryItemAndPricingView] iiapv WITH (NOLOCK)
  91.                           JOIN InventoryStockTotalView istv WITH (NOLOCK) ON IIAPV.ItemCode = istv.ItemCode
  92.                         WHERE iiapv.LanguageCode = ''' + @LanguageCode + '''
  93.                            AND istv.LanguageCode = ''' + @LanguageCode + '''
  94.                            AND iiapv.ItemType IN (''Non-Stock'', ''Service'', ''Stock'',''Service'',''Electronic Download'')
  95.                ) H ON H.ItemCode = a.ItemCode           
  96. INNER JOIN InventoryItemDescription IID WITH (NOLOCK) ON IID.ItemCode = A.ItemCode AND IID.LanguageCode = ''' + @LanguageCode + '''           
  97. INNER JOIN InventoryItemWebOption IIWO WITH (NOLOCK) ON IIWO.ItemCode = A.ItemCode AND IIWO.WebsiteCode = ''' + @WebsiteCode + '''          
  98. INNER JOIN InventoryItemWebOptionDescription IIWOD WITH (NOLOCK) ON IIWOD.ItemCode = IIWO.ItemCode AND IIWOD.WebsiteCode = IIWO.WebsiteCode  AND IIWOD.LanguageCode = ''' + @LanguageCode + ''' ' + @JoinUnitMeasureString + ' ' + @EntityBuilderString + '
  99. WHERE (IID.ItemDescription LIKE ' + @SearchTerm + ' OR A.ItemName LIKE ' + @SearchTerm + ' ' + @SearchDescriptionString + ') AND     
  100.     ItemType IN (''Non-Stock'', ''Service'', ''Stock'',''Matrix Group'',''Kit'',''Service'',''Electronic Download'', ''Assembly'') AND ''' +
  101.    cast(@CurrentDate as nvarchar(50)) + ''' between isnull(IIWO.StartDate, ''1/1/1900'') AND isnull(IIWO.EndDate, ''12/31/9999'') '
  102.  
  103. IF (@ShowDiscontinued = 0) BEGIN
  104.     SET @SQL = @SQL + ' AND A.Status = ''A'' '
  105. END
  106.  
  107. SET @SQL = @SQL + ' AND IIWO.Published = 1 AND IIWO.CheckOutOption = 0 AND         
  108.        ItemType LIKE ' + @ItemType + 'ORDER BY IIWO.IsExclusive DESC, A.ItemName ASC'
  109.   
  110. EXEC(@SQL)  
  111. --PRINT @SQL
  112.  
  113. END

Now open your XmlPackages\page.search.xml.config and change the following block:

  1. <query name="Products" rowElementName="Product" runif="SearchTerm">
  2.   <sql>
  3.     <![CDATA[
  4.           exec aspdnsf_WebSearchInventory @SearchTerm, @WebSiteCode, @LanguageCode, NULL, 'ANY', 'ANY', '0', '0', '0',@SearchDescriptions, @CurrentDate
  5.         ]]>
  6.   </sql>
  7.   <queryparam paramname="@SearchTerm" paramtype="request" requestparamname="SearchTerm" sqlDataType="varchar" defvalue="" validationpattern=""/>
  8.   <queryparam paramname="@WebSiteCode" paramtype="runtime" requestparamname="WebSiteCode" sqlDataType="varchar" defvalue="" validationpattern=""/>
  9.   <queryparam paramname="@LanguageCode" paramtype="runtime" requestparamname="LanguageCode" sqlDataType="varchar" defvalue="" validationpattern=""/>
  10.   <queryparam paramname="@SearchDescriptions" paramtype="runtime" requestparamname="SearchDescriptions" sqlDataType="varchar" defvalue="0" validationpattern=""/>
  11.   <queryparam paramname="@CurrentDate" paramtype="runtime" requestparamname="Date" sqlDataType="datetime" defvalue="0" validationpattern=""/>
  12. </query>

to this

  1. <query name="Products" rowElementName="Product" runif="SearchTerm">
  2.   <sql>
  3.     <![CDATA[
  4.           exec ttx_WebSearchInventoryExt @SearchTerm, @WebSiteCode, @LanguageCode, NULL, 'ANY', 'ANY', '0', '0', '0',@SearchDescriptions, @CurrentDate, @ShowDiscontinued
  5.         ]]>
  6.   </sql>
  7.   <queryparam paramname="@SearchTerm" paramtype="request" requestparamname="SearchTerm" sqlDataType="varchar" defvalue="" validationpattern=""/>
  8.   <queryparam paramname="@WebSiteCode" paramtype="runtime" requestparamname="WebSiteCode" sqlDataType="varchar" defvalue="" validationpattern=""/>
  9.   <queryparam paramname="@LanguageCode" paramtype="runtime" requestparamname="LanguageCode" sqlDataType="varchar" defvalue="" validationpattern=""/>
  10.   <queryparam paramname="@SearchDescriptions" paramtype="runtime" requestparamname="SearchDescriptions" sqlDataType="varchar" defvalue="0" validationpattern=""/>
  11.   <queryparam paramname="@CurrentDate" paramtype="runtime" requestparamname="Date" sqlDataType="datetime" defvalue="0" validationpattern=""/>
  12.   <queryparam paramname="@ShowDiscontinued" paramtype="request" requestparamname="ShowDiscontinued" sqlDataType="varchar" defvalue="1" validationpattern=""/>
  13. </query>

and you should be ready to go.  Note, you can add a ‘ShowDiscontinued=0’ or ‘ShowDiscontinued=1’ to show or hide discontinued products. 

Labeling Out of Stock or Discontinued Products

So now that we added discontinued products, we can’t tell quickly what is discontinued or not.  And while we are at it, why not fix the same issue for stock problems.  Let’s throw in some price displays while we are at it.

These are all simple changes to the XmlPackages\page.search.xml.config.  Around line 158, we need to add two columns to our Product Results header row:

  1. <tr>
  2.     <td>
  3.         <b>Notes</b>                                                   
  4.     </td>
  5.     <td align="left">
  6.     <b>
  7.         <xsl:value-of select="ise:StringResource('search.aspx.6', $LocaleSetting)" disable-output-escaping="yes"/>
  8.     </b>
  9.     </td>
  10.     <td>
  11.         <b>Price</b>
  12.     </td>
  13.     <td align="center">
  14.     <b>
  15.         <xsl:value-of select="ise:StringResource('AppConfig.CategoryPromptSingular', $LocaleSetting)" disable-output-escaping="yes"/>
  16.     </b>
  17.     </td>
  18.     <xsl:if test="ise:AppConfigBool('Search_ShowManufacturersInResults')='true'">
  19.     <td align="center">
  20.         <b>
  21.         <xsl:value-of select="ise:StringResource('search.aspx.8', $LocaleSetting)" disable-output-escaping="yes"/>
  22.         </b>
  23.     </td>
  24.     </xsl:if>
  25. </tr>
  26. <xsl:apply-templates select="/root/Products/Product"/>

And now, in your product template (starting at line ~202), right after the inner-most <tr> element of the product table, add the following:

  1. <tr>
  2.     <xsl:choose>
  3.         <xsl:when test="Status != 'A' and UnitsInStock = 0">
  4.             <xsl:attribute name="class">searchResult discontinued outOfStock</xsl:attribute>
  5.         </xsl:when>
  6.         <xsl:when test="Status != 'A'">
  7.             <xsl:attribute name="class">searchResult discontinued</xsl:attribute>
  8.         </xsl:when>
  9.         <xsl:when test="UnitsInStock = 0">
  10.             <xsl:attribute name="class">searchResult outOfStock</xsl:attribute>
  11.         </xsl:when>
  12.         <xsl:otherwise>
  13.             <xsl:attribute name="class">searchResult</xsl:attribute>
  14.         </xsl:otherwise>
  15.     </xsl:choose>

add then following columns; a Notes column before the product name column:

  1. <td width="110px">
  2.     <span style="font-weight:normal;">
  3.         <xsl:choose>
  4.             <xsl:when test="Status != 'A' and UnitsInStock = 0">
  5.                 <span style="background: #F2F2F2;">Discontinued</span>
  6.                 <br/>
  7.                 <span style="background: #F2DCDB;">Out of Stock</span>
  8.             </xsl:when>
  9.             <xsl:when test="Status != 'A'">
  10.                 <span style="background: #F2F2F2;">Discontinued</span>
  11.             </xsl:when>
  12.             <xsl:when test="UnitsInStock = 0">
  13.                 <span style="background: #F2DCDB;">Out of Stock</span>
  14.             </xsl:when>
  15.         </xsl:choose>
  16.     </span>
  17. </td>

And the price column after the product name column:

  1. <td align="right" width="65px" >
  2.     <xsl:choose>
  3.         <xsl:when test="Status != 'A' and UnitsInStock = 0">
  4.             <span style="background: #F2F2F2;">
  5.                 $ <xsl:value-of select="format-number(RetailPrice, '#,##0.00')" />
  6.             </span>
  7.         </xsl:when>
  8.         <xsl:when test="Status != 'A'">
  9.             <span style="background: #F2F2F2;">
  10.                 $ <xsl:value-of select="format-number(RetailPrice, '#,##0.00')" />
  11.             </span>
  12.         </xsl:when>
  13.         <xsl:when test="UnitsInStock = 0">
  14.             <span style="background: #F2DCDB;">
  15.                 $ <xsl:value-of select="format-number(RetailPrice, '#,##0.00')" />
  16.             </span>
  17.         </xsl:when>
  18.         <xsl:otherwise>
  19.             <span >
  20.                 $ <xsl:value-of select="format-number(RetailPrice, '#,##0.00')" />
  21.             </span>
  22.         </xsl:otherwise>
  23.     </xsl:choose>
  24. </td>

Now you have the price and pretty notes for the stock status:

image

One Word: Checkboxes!!!

Now that we have all that done, we can use some simple jQuery magic to add some checkboxes that hide discontinued and/or out of stock items!

As part of this, we can refactor the search text to a format that is easier to extend (table system that breaks everything out) and makes it easily to match visuals with all our users’ browsers.  Replace the the Form element located around Line 76 of XmlPackages\page.search.xml.config with the following:

  1. <form method="GET" action="search.aspx" id="SearchForm2" name="SearchForm2">
  2.         <table border="0" cellpadding="0" cellspacing="0" width="500px">
  3.             <tr>
  4.                 <td colspan="2">
  5.                     <span style="color:red;font-weight:bold">
  6.                         <xsl:value-of select="/root/QueryString/errormsg" />
  7.                     </span>
  8.                 </td>
  9.             </tr>
  10.             <tr>
  11.                 <td colspan="2">
  12.                     <xsl:value-of select="ise:StringResource('search.aspx.3', $LocaleSetting)" disable-output-escaping="yes" />&#0160;
  13.                 </td>
  14.             </tr>
  15.             <tr align="left">
  16.                 <td width="300px">
  17.                     <input type="text" id="SearchTerm" name="SearchTerm" size="50" maxlength="70" value="{$pSearchTerm}" style="width:300px;"></input>
  18.                     <br/>
  19.                     <xsl:value-of select="ise:GetSearchFormValidatorScript('SearchForm', 'SearchTerm')" disable-output-escaping="yes" />
  20.                 </td>
  21.                 <td width="200px">
  22.                     <input type="submit" value="Search" name="B1" />
  23.                 </td>
  24.             </tr>
  25.             <tr align="">
  26.                 <td></td>
  27.                 <td>
  28.                     <input id="ShowDiscontinued" type="checkbox">Show Discontinued</input>
  29.                 </td>
  30.             </tr>
  31.             <tr align="">
  32.                 <td></td>
  33.                 <td>
  34.                     <input id="ShowOutOfStock" type="checkbox" checked="checked">Show Out of Stock</input>
  35.                 </td>
  36.             </tr>
  37.         </table>
  38. </form>

which gives us some checkboxes, but now we have to wire them up.  Put this script with your <xsl:template match=”/”>

  1.       <xsl:template match="/">
  2.         <xsl:value-of select="ise:Topic('SearchPageHeader')" disable-output-escaping="yes"/>
  3.           <script type="text/javascript">
  4. function UpdateVisibility()
  5. {
  6. $(".searchResult").css("display", "table-row");
  7.  
  8. if ($("#ShowDiscontinued").is(':checked') == false)
  9.     $(".discontinued").css("display", "none");
  10.  
  11. if ($("#ShowOutOfStock").is(':checked') == false)
  12.     $(".outOfStock").css("display", "none");
  13. };
  14.              
  15. $(document).ready(function()
  16. {
  17. $("#ShowDiscontinued").click(UpdateVisibility);
  18. $("#ShowOutOfStock").click(UpdateVisibility);
  19. UpdateVisibility();
  20. });
  21.           </script>

Now, save everything and if you did it right (I know my instructions are not the easiest out there), you have some awesome stuff:

image

And if you uncheck the “Show Out of Stock” checkbox, all my products go away!

image

Pretty cool for an hour’s effort huh?  Maybe my next post will be table sorting…

Tags:

Blog

Displaying a Stock Table for Matrix Items in Interprise 5.6.22

by jasonrshaver 8. September 2011 00:31

Some of the products we list have 18-24 permeations and we want to show customers how many we might have in stock currently.  Luckily with XmlPackages, this is easy to do.

First, let’s look at the result:

image

Setting Up the Database

The first step to our result is to get the current stock information into our XmlPackage via a Query.  There are two way to do this, rowsets and XML.  A rowset is the common ‘table’ of information of from the database.  This is how most other packages work.  When looking for help on using XML result sets for Interprise, I doubt there are any existing samples, so I figured I might as well be first. 

** Warning ** This method is complex and may not be the “best” method, but it is the one I went with.

First, how do we get SQL to produce well structured XML data.  Well, if you are rocking SQL Server 2005 or latter and understand the T-SQL syntax, this is simple with the ‘FOR XML’ keywords.  Here is my stored procedure for matrix items using up to 3 attributes (Sorry for the scroll bars, but it is crazy long):

  1. CREATE PROCEDURE [dbo].[ttx_GetItemInventoryXml]
  2.     @ItemCode nvarchar(30)
  3. AS
  4. BEGIN
  5.     SET NOCOUNT ON;
  6.  
  7.     DECLARE @AttributeCount int = 1
  8.     DECLARE @Result nvarchar(max)
  9.  
  10.     SELECT @AttributeCount = COUNT(*)
  11.         FROM InventoryItem i WITH (NOLOCK)
  12.         INNER JOIN InventoryAttribute ia WITH (NOLOCK) ON ia.ItemCode = i.ItemCode
  13.         WHERE i.ItemCode = @ItemCode
  14.  
  15.     IF (@AttributeCount = 0) BEGIN
  16.         SELECT '' -- This is built for Matrix items only right now
  17.     END
  18.  
  19.     IF (@AttributeCount = 1) BEGIN
  20.             DECLARE @MyTempTable1 TABLE
  21.             (
  22.                  Tag int
  23.                , Parent int
  24.                , [Item!1!ItemCode] nvarchar(30)
  25.                , [Attribute1!2!Attribute1] nvarchar(255)
  26.                , [Attribute1!2!AttributeCode1] nvarchar(255)
  27.                , [Attribute2!3] decimal
  28.             );
  29.  
  30.            insert into @MyTempTable1
  31.                 SELECT 1 AS 'Tag', NULL AS 'Parent'
  32.                      , imi.ItemCode AS [Item!1!ItemCode]
  33.                      , NULL AS [Attribute1!2!Attribute1]
  34.                      , NULL AS [Attribute1!2!AttributeCode1]
  35.                      , NULL AS [Attribute2!2]               
  36.                   
  37.                   from InventoryItem ii WITH (NOLOCK)
  38.              LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  39.              LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  40.                  WHERE imi.ItemCode = @ItemCode
  41.  
  42.                  UNION
  43.                        
  44.                        SELECT 2 AS 'Tag', 1 AS 'Parent', imi.ItemCode
  45.                          , imi.Attribute1, imi.AttributeCode1
  46.                          , isv.UnitsInStock
  47.                       from InventoryItem ii WITH (NOLOCK)
  48.                  LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  49.                  LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  50.                      WHERE imi.ItemCode = @ItemCode
  51.                                        
  52.              ORDER BY [Item!1!ItemCode]
  53.                     , [Attribute1!2!Attribute1]
  54.                     , [Attribute1!2!AttributeCode1]
  55.             
  56.             SET @Result = (SELECT * FROM @MyTempTable1 FOR XML EXPLICIT)
  57.             SELECT @Result
  58.     END
  59.  
  60.     IF (@AttributeCount = 2) BEGIN
  61.  
  62.             DECLARE @MyTempTable2 TABLE
  63.             (
  64.                  Tag int
  65.                , Parent int
  66.                , [Item!1!ItemCode] nvarchar(30)
  67.                , [Attribute1!2!Attribute1] nvarchar(255)
  68.                , [Attribute1!2!AttributeCode1] nvarchar(255)
  69.                , [Attribute2!3!Attribute2] nvarchar(255)
  70.                , [Attribute2!3!AttributeCode2] nvarchar(255)
  71.                , [Attribute3!4] decimal
  72.             );
  73.  
  74.            insert into @MyTempTable2
  75.                 SELECT 1 AS 'Tag', NULL AS 'Parent'
  76.                      , imi.ItemCode AS [Item!1!ItemCode]
  77.                      , NULL AS [Attribute1!2!Attribute1]
  78.                      , NULL AS [Attribute1!2!AttributeCode1]
  79.                      , NULL AS [Attribute2!3!Attribute2]
  80.                      , NULL AS [Attribute2!3!AttributeCode2]
  81.                      , NULL AS [Attribute3!3]               
  82.                   
  83.                   from InventoryItem ii WITH (NOLOCK)
  84.              LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  85.              LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  86.                  WHERE imi.ItemCode = @ItemCode
  87.  
  88.                  UNION
  89.                        
  90.                        SELECT 2 AS 'Tag', 1 AS 'Parent', imi.ItemCode
  91.                          , imi.Attribute1, imi.AttributeCode1
  92.                          , NULL, NULL
  93.                          , NULL
  94.                       from InventoryItem ii WITH (NOLOCK)
  95.                  LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  96.                  LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  97.                      WHERE imi.ItemCode = @ItemCode
  98.                        
  99.                      UNION
  100.                                        
  101.                             SELECT 3 AS 'Tag', 2 AS 'Parent', imi.ItemCode
  102.                                  , imi.Attribute1, imi.AttributeCode1
  103.                                  , imi.Attribute2, imi.AttributeCode2
  104.                                  , isv.UnitsInStock
  105.                               from InventoryItem ii WITH (NOLOCK)
  106.                          LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  107.                          LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  108.                              WHERE imi.ItemCode = @ItemCode
  109.                                        
  110.              ORDER BY [Item!1!ItemCode]
  111.                     , [Attribute1!2!Attribute1]
  112.                     , [Attribute1!2!AttributeCode1]
  113.                     , [Attribute2!3!Attribute2]
  114.             
  115.             SET @Result = (SELECT * FROM @MyTempTable2 FOR XML EXPLICIT)
  116.             SELECT @Result
  117.     END
  118.  
  119.     IF (@AttributeCount = 3) BEGIN
  120.             
  121.             DECLARE @MyTempTable3 TABLE
  122.             (
  123.                  Tag int
  124.                , Parent int
  125.                , [Item!1!ItemCode] nvarchar(30)
  126.                , [Attribute1!2!Attribute1] nvarchar(255)
  127.                , [Attribute1!2!AttributeCode1] nvarchar(255)
  128.                , [Attribute2!3!Attribute2] nvarchar(255)
  129.                , [Attribute2!3!AttributeCode2] nvarchar(255)
  130.                , [Attribute3!4!Attribute3] nvarchar(255)
  131.                , [Attribute3!4!AttributeCode3] nvarchar(255)
  132.                , [Attribute3!4] decimal
  133.             );
  134.  
  135.            insert into @MyTempTable3
  136.                 SELECT 1 AS 'Tag', NULL AS 'Parent'
  137.                      , imi.ItemCode AS [Item!1!ItemCode]
  138.                      , NULL AS [Attribute1!2!Attribute1]
  139.                      , NULL AS [Attribute1!2!AttributeCode1]
  140.                      , NULL AS [Attribute2!3!Attribute2]
  141.                      , NULL AS [Attribute2!3!AttributeCode2]
  142.                      , NULL AS [Attribute3!4!Attribute3]
  143.                      , NULL AS [Attribute3!4!AttributeCode3]
  144.                      , NULL AS [Attribute3!4]               
  145.                   
  146.                   from InventoryItem ii WITH (NOLOCK)
  147.              LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  148.              LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  149.                  WHERE imi.ItemCode = @ItemCode
  150.  
  151.                  UNION
  152.                        
  153.                        SELECT 2 AS 'Tag', 1 AS 'Parent', imi.ItemCode
  154.                          , imi.Attribute1, imi.AttributeCode1
  155.                          , NULL, NULL
  156.                          , NULL, NULL
  157.                          , NULL
  158.                       from InventoryItem ii WITH (NOLOCK)
  159.                  LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  160.                  LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  161.                      WHERE imi.ItemCode = @ItemCode
  162.                        
  163.                      UNION
  164.                                        
  165.                             SELECT 3 AS 'Tag', 2 AS 'Parent', imi.ItemCode
  166.                                  , imi.Attribute1, imi.AttributeCode1
  167.                                  , imi.Attribute2, imi.AttributeCode2
  168.                                  , NULL, NULL
  169.                                  , NULL
  170.                               from InventoryItem ii WITH (NOLOCK)
  171.                          LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  172.                          LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  173.                              WHERE imi.ItemCode = @ItemCode
  174.                                        
  175.                              UNION
  176.                                     SELECT 4 AS 'Tag', 3 AS 'Parent', imi.ItemCode
  177.                                          , imi.Attribute1 , imi.AttributeCode1
  178.                                          , imi.Attribute2, imi.AttributeCode2
  179.                                          , imi.Attribute3, imi.AttributeCode3
  180.                                          , isv.UnitsInStock
  181.                                       from InventoryItem ii WITH (NOLOCK)
  182.                                  LEFT JOIN InventoryStockTotal isv WITH (NOLOCK) ON ii.ItemCode = isv.ItemCode
  183.                                  LEFT JOIN InventoryMatrixItem imi WITH (NOLOCK) ON ii.ItemCode = imi.MatrixItemCode
  184.                                      WHERE imi.ItemCode = @ItemCode
  185.              ORDER BY [Item!1!ItemCode]
  186.                     , [Attribute1!2!Attribute1]
  187.                     , [Attribute1!2!AttributeCode1]
  188.                     , [Attribute2!3!Attribute2]
  189.                     , [Attribute3!4!Attribute3]
  190.             
  191.             SET @Result = (SELECT * FROM @MyTempTable3 FOR XML EXPLICIT)
  192.             SELECT @Result
  193.     END
  194.     
  195.     -- We don't support more than 3 attributes, but it is very easy to extend
  196.     -- this by following the pattern above.
  197.     IF (@AttributeCount > 3) BEGIN
  198.         SELECT ''
  199.     END
  200. END

And the output is a single row with a single column that looks like the following:

  1. <Item ItemCode="ITEM-000001">
  2.   <Attribute1 Attribute1="Black" AttributeCode1="Color">
  3.     <Attribute2 Attribute2="33 deg" AttributeCode2="Hardness">
  4.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">15.000000</Attribute3>
  5.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  6.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  7.     </Attribute2>
  8.     <Attribute2 Attribute2="37 deg" AttributeCode2="Hardness">
  9.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">0.000000</Attribute3>
  10.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  11.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  12.     </Attribute2>
  13.     <Attribute2 Attribute2="41 deg" AttributeCode2="Hardness">
  14.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">1.000000</Attribute3>
  15.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  16.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  17.     </Attribute2>
  18.   </Attribute1>
  19.   <Attribute1 Attribute1="Red" AttributeCode1="Color">
  20.     <Attribute2 Attribute2="33 deg" AttributeCode2="Hardness">
  21.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">0.000000</Attribute3>
  22.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  23.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  24.     </Attribute2>
  25.     <Attribute2 Attribute2="37 deg" AttributeCode2="Hardness">
  26.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">0.000000</Attribute3>
  27.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  28.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  29.     </Attribute2>
  30.     <Attribute2 Attribute2="41 deg" AttributeCode2="Hardness">
  31.       <Attribute3 Attribute3="1.8mm" AttributeCode3="Thickness">0.000000</Attribute3>
  32.       <Attribute3 Attribute3="2.0mm" AttributeCode3="Thickness">0.000000</Attribute3>
  33.       <Attribute3 Attribute3="2.3mm" AttributeCode3="Thickness">0.000000</Attribute3>
  34.     </Attribute2>
  35.   </Attribute1>
  36. </Item>

Updating our XmlPackage

Now, how do we use that block of xml in our XmlPackages?  Well first, I will be modifying XmlPackages\product.matrixproduct.xml.config

Make sure you backup your XmlPackages before changing them!  You have been warned. 

First, lets add our Query to the Package element right before our PackageTransform element:

  1. <query name="Inventory" rowElementName="Item" retType="xml">
  2.     <sql>
  3.         <![CDATA[
  4.             exec ttx_GetItemInventoryXml @ItemCode
  5.         ]]>
  6.     </sql>
  7.     <queryparam paramname="@ItemCode" paramtype="runtime" requestparamname="ItemCode" sqlDataType="nvarchar" defvalue="" validationpattern=""/>
  8. </query>

What this does is call our stored procedure passing in the ItemCode.  Make special note of the retType attribute, this is what tells the XmlPackage to view the result set as XML.

Now to use that XML data, here is a xsl:template showing how to do two common outputs, a list and a table.  The XSL is hardcoded to use 3 attributes, but you can easily modify it to use any number of attributes instead.  If anyone can find a better way to ‘layout’ the table without using 10 xsl:for-each elements, please let me know. 

  1. <xsl:template name="InventoryTable">
  2.     <div class="ttxStockTable">       
  3.         
  4.         <!-- Uncomment this for a simple listing of products -->
  5.         <!--<xsl:for-each select="/root/Inventory/Item/Attribute1">
  6.             <xsl:for-each select="Attribute2">
  7.                 <xsl:for-each select="Attribute3">
  8.                     <xsl:value-of select="../../@AttributeCode1"/>: <xsl:value-of select="../../@Attribute1"/>,
  9.                     <xsl:value-of select="../@AttributeCode2"/>: <xsl:value-of select="../@Attribute2"/>,
  10.                     <xsl:value-of select="@AttributeCode3"/>: <xsl:value-of select="@Attribute3"/>,
  11.                     <xsl:value-of select="." /><br/>
  12.                 </xsl:for-each>
  13.             </xsl:for-each>
  14.         </xsl:for-each>-->
  15.  
  16.         <!-- Uncomment this for a pretty table of products -->
  17.         <table>
  18.             <tbody>
  19.                 <xsl:for-each select="/root/Inventory/Item[1]">
  20.                     <tr>
  21.                         <th></th>
  22.                         <xsl:for-each select="Attribute1" >
  23.                             <th class="headerName">
  24.                                 <xsl:attribute name="colspan">
  25.                                     <xsl:value-of select="count(Attribute2)" />
  26.                                 </xsl:attribute>
  27.                                 <xsl:value-of select="@Attribute1" />
  28.                             </th>
  29.                         </xsl:for-each>
  30.                     </tr>
  31.                 </xsl:for-each>
  32.  
  33.                 <xsl:for-each select="/root/Inventory/Item[1]">
  34.                     <tr>
  35.                         <th></th>
  36.                         <xsl:for-each select="/root/Inventory/Item[1]/Attribute1">
  37.                             <xsl:for-each select="Attribute2" >
  38.                                 <th class="headerName">
  39.                                     <xsl:value-of select="@Attribute2" />
  40.                                 </th>
  41.                         </xsl:for-each>
  42.                     </xsl:for-each>
  43.                     </tr>
  44.                 </xsl:for-each>
  45.  
  46.                 <xsl:for-each select="/root/Inventory/Item[1]/Attribute1[1]/Attribute2[1]/Attribute3">
  47.                     <tr>
  48.                         <xsl:variable name="CurrentAtt3" select="@Attribute3" />
  49.                         <td class="sizeLabel">
  50.                             <xsl:value-of select="@Attribute3"/>
  51.                         </td>
  52.                         <xsl:for-each select="/root/Inventory/Item/Attribute1">
  53.                             <xsl:for-each select="Attribute2">
  54.                                 <xsl:for-each select="Attribute3[@Attribute3=$CurrentAtt3]">
  55.                                     <!-- Uncomment this to give exact stock levels instead of hints -->
  56.                                     <!--<td><xsl:value-of select="." /></td>-->
  57.                                     <xsl:choose>
  58.                                         <xsl:when test=". = 1">
  59.                                             <td class="StockHint StockHint_LowStock">
  60.                                                 Low Stock
  61.                                             </td>
  62.                                         </xsl:when>
  63.                                         <xsl:when test=". = 2">
  64.                                             <td class="StockHint StockHint_InStock">
  65.                                                 In Stock
  66.                                             </td>
  67.                                         </xsl:when>
  68.                                         <xsl:when test=". >= 3">
  69.                                             <td class="StockHint StockHint_InStock">
  70.                                                 In Stock
  71.                                             </td>
  72.                                         </xsl:when>
  73.                                         <xsl:otherwise>
  74.                                             <td class="notInStock">
  75.                                             </td>
  76.                                         </xsl:otherwise>
  77.                                     </xsl:choose>
  78.                                 </xsl:for-each>
  79.                             </xsl:for-each>
  80.                         </xsl:for-each>
  81.                     </tr>
  82.                 </xsl:for-each>
  83.             </tbody>
  84.         </table>
  85.     </div>
  86. </xsl:template>

And finally, lets call that template in our product.  I put my right after the description (Line ~130) like so:

  1. <xsl:value-of select="$pDescription" disable-output-escaping="yes"/>
  2. <xsl:call-template name="InventoryTable" />

So, that was not too hard, and I hope this gives everyone the courage to start doing awesome stuff with their XmlPackages.  There is an endless supply of data out there and you can access it all!

Tags:

Bypassing the Interprise 5.6.22 Menu’s RootNode

by JasonRShaver 7. September 2011 18:53

The default method to handing the asp:Menu is to have a single RootNode (default is ‘Home’) and use hover-over menus to drill down.  In my case, I wanted to have a row of items going across the top of my webpage like so:

image

instead of the default layout that looks like this:

image

Understanding App_Code\ISESiteMapProvider.cs

So, software such as Interprise is special.  It can’t just use a normal SiteMap stuck in a file somewhere.  They need to be dynamic.  The Categories and Manufacturers are obvious, but what about showing something like the nearest store?  Or how about if you are a “Woot” style company and want to have a “Quantity Remaining: 481” link on your menu?  To allow all this functionality, Interprise turned to XmlPackages. 

XmlPackages, and how they work, are outside the scope of this post, but they allow you to take a group of data sources (Query strings, SQL Servers, RSS Feeds, Locale information, etc.) and combine them using XML/XSLT to render content (usually HTML or XML). 

Interprise uses a two step system for its SiteMap.  First the SkinBase gets the contents of XmlPackages\page.menu.xml.config via App_Code\ISESiteMapFactory.cs.  Then it takes that information, localizes some strings (everything with the (!menu.YourAccount!) style syntax) via your string tables, and finally it pulls all your categories, section, and/or manufacturers.  This is all done with magic in App_Code\ISESiteMapProvider.cs.

Now, to reach our goal, you would be thinking that all we need to do is change the starting node used in ISESiteMapProvider, but doing so creates an invalid sitemap (more on this in a bit).  So, we leave this alone for now…

Setting Up the New SiteMap

First thing we need to do is edit the default SiteMap to move ‘Home’ alongside the other categories and create a new empty RootNode.  We need a RootNode because Microsoft’s standard requires one, and only one, RootNode.  While I have linked to the docs, it is good to point out that Urls must be unique, you cannot have two links pointing to the same page in a single SiteMap.

As I mentioned above, the required file is located at XmlPackages\page.menu.xml.config.  Here is an example after moving Home to be along the rest of my nodes and adding a new RootNode to be ignored:

  1. <xsl:template match="/">
  2.     <siteMap>
  3.         <siteMapNode title="this gets ignored">
  4.             <siteMapNode title="Home"  url="default.aspx" />
  5.             <!-- Titles are predefined entities, the contents are not explicitly set here but runtime they are dynamically populated per entity-->
  6.             <siteMapNode title="(!menu.Categories!)" />
  7.             <siteMapNode title="(!menu.Manufacturers!)" url="manufacturers.aspx" />
  8.             <!-- These are static links -->
  9.             <siteMapNode title="(!menu.CustomerService!)" url="t-service.aspx" >
  10.                 <siteMapNode title="(!menu.YourAccount!)" url="account.aspx" />
  11.                 <siteMapNode title="(!menu.FAQs!)" url="t-faq.aspx" />
  12.                 <siteMapNode title="(!menu.PolicyReturns!)" url="t-returns.aspx" />
  13.                 <siteMapNode title="(!menu.Shipping!)" url="t-shipping.aspx" />
  14.                 <siteMapNode title="(!menu.Contact!)" url="t-contact.aspx" />
  15.                 <siteMapNode title="(!menu.PolicyPrivacy!)" url="t-privacy.aspx" />
  16.                 <siteMapNode title="(!menu.PolicySecurity!)" url="t-security.aspx" />
  17.                 <siteMapNode title="(!menu.LeadForm!)" url="t-Leadform.aspx" />
  18.             </siteMapNode>
  19.         </siteMapNode>
  20.     </siteMap>
  21. </xsl:template>

Changing App_Code\SkinBase.cs

Now that we have our new and improve SiteMap, we need to tell our SkinBase class to handle the SiteMapNode information explicitly instead of relying on data binding.  Again, this is required because even if we wanted to replace the ISESiteMapProvider, we still would be stuck with a single root node.

Edit the SetupTelerikMenu() method inside App_Code\SkinBase.cs replacing the commented lines with this block:

  1. // Explicit Menu setup inside SetupTelerikMenu()
  2. SiteMapDataSource ds = new SiteMapDataSource();
  3. ds.Provider = ISESiteMapProviderFactory.Instance.GetProvider(ThisCustomer.LocaleSetting);
  4. //telerikMenu.DataSource = ds;
  5. //telerikMenu.DataBind();
  6. SetupRadMenu(telerikMenu, ds.Provider as SiteMapProvider);

If you are trying to use the asp:Menu control instead of the Telerik control you would want to edit SetupMenu() instead.

Now we just have to add the SetupRadMenu method and its related code.  Again, if you are using the asp:Menu instead, just change instances of RadMenu to System.Web.UI.WebControls.Menu.  Add these methods to your App_Code\SkinBase.cs:

  1. private void SetupRadMenu(RadMenu menu, SiteMapProvider provider)
  2. {
  3.     // Use this one for a single root
  4.     //BuildNodes(menu, null, provider.RootNode);
  5.  
  6.     // Use this one for multiple roots
  7.     foreach (SiteMapNode node in provider.RootNode.ChildNodes)
  8.         BuildNodes(menu, null, node);
  9. }
  10. public void BuildNodes(RadMenu menu, RadMenuItem item, SiteMapNode node)
  11. {
  12.     var CurrentItem = AddNodeToMenu(menu, item, node);
  13.  
  14.     foreach (SiteMapNode CurrentChildNode in node.ChildNodes)
  15.     {
  16.         BuildNodes(menu, CurrentItem, CurrentChildNode);
  17.     }
  18. }
  19.  
  20. private RadMenuItem AddNodeToMenu(RadMenu menu, RadMenuItem item, SiteMapNode node)
  21. {
  22.     var ThisItem = new RadMenuItem();
  23.     ThisItem.Text = node.Title;
  24.     ThisItem.NavigateUrl = node.Url;
  25.  
  26.     if (item == null)
  27.         menu.Items.Add(ThisItem);
  28.     else
  29.         item.Items.Add(ThisItem);
  30.  
  31.     return ThisItem;
  32. }

and that’s it for SkinBase.  Notice that I left the ability to revert back to a single root node just by uncommenting a section of SetupRadMenu and commenting out the other.

Skin Template.ascx

And just for completeness, here is what your Templace.ascx might look like.  Again, I am using a Telerik menu instead of the ASP.NET Menu, but you get the idea I am sure:

  1. <telerik:RadMenu runat="server" ID="aspnetMenu" Skin="TTX" EnableEmbeddedSkins="false" />

Notice that there is nothing ‘special’ here, everything is handled in SkinBase.

Next Steps

If you are following my series here, you will notice that we have a Telerik menu, and now we have complete control over what we are displaying on it, while still using the dynamic items from the ISESiteMapProvider.cs.  But where can we take this?  Here is my next step:

image

But that will have to wait until I get a bit more time to write everything down.  But trust me, it might be awesome.

Tags: , , , , ,

Blog

Add Telerik ASP.NET AJAX Menu to Interprise 5.6.22

by jasonrshaver 6. September 2011 21:43

Setup

This example is assuming Interprise 5.6.22 and Telerik.Web.UI version 2011.2.712, but any recent Telerik version should work fine.

Add the following DLLs to the \bin directory.  You can locate them in your \Program Files directory.

  • Telerik.Web.Design.dll (not needed for most people, but might as well put it there)
  • Telerik.Web.UI.dll
  • Telerik.Web.UI.Skins.dll (not needed if you don’t want to use any embedded skins)
  • Telerik.Web.UI.XML

Customize App_Code\SkinBase.cs

Edit your App_Code\SkinBase.cs file, but make sure you make a backup.  It is really easy to kill your site.  Also note that if you start changing this file on a live site, you can cause errors for customers who happen to be using the site at the time. 

Add the following code to the “Find Menu” region (around Line 592)

  1. private void FindTelerikMenu(Control context, List<WebControl> foundMenus)
  2. {
  3.     foreach (Control ctrl in context.Controls)
  4.     {
  5.         if (ctrl is Telerik.Web.UI.RadMenu) // || ctrl is ComponentArt.Web.UI.Menu)
  6.         {
  7.             foundMenus.Add(ctrl as WebControl);
  8.         }
  9.         else
  10.         {
  11.             // recurse  
  12.             FindTelerikMenu(ctrl, foundMenus);
  13.         }
  14.     }
  15. }

Add the following code to the “SetupMenu” region (around line 432)

  1. private void SetupTelerikMenu()
  2. {
  3.     HtmlForm frm = this.ThisForm;
  4.     if (frm == null)
  5.         return;
  6.  
  7.     if (frm.FindControl("ScriptManager") == null)
  8.     {
  9.         Panel pnlScriptManager = new Panel();
  10.         pnlScriptManager.ID = "pnlTelerikScriptManager";
  11.  
  12.         RadScriptManager manager = new RadScriptManager();
  13.         manager.ID = "ScriptManager";
  14.         manager.EnableScriptCombine = false;
  15.         frm.Controls.AddAt(0, manager);
  16.  
  17.         // allow page to register scripts and web services
  18.         RegisterScriptsAndServices(manager);
  19.     }
  20.  
  21.     List<WebControl> foundMenus = new List<WebControl>();
  22.     FindTelerikMenu(this, foundMenus);
  23.  
  24.     if (foundMenus.Count == 0)
  25.         return;
  26.  
  27.     string randomId = Guid.NewGuid().ToString("N").Substring(0, 5);
  28.     // make ready the scripts
  29.     StringBuilder script = new StringBuilder();
  30.     script.AppendFormat("<script type=\"text/javascript\" language=\"Javascript\">\n");
  31.     script.AppendFormat("    function loadMenu_{0}() {{\n", randomId);
  32.  
  33.     foreach (WebControl foundMenu in foundMenus)
  34.     {
  35.         Telerik.Web.UI.RadMenu telerikMenu = foundMenu as Telerik.Web.UI.RadMenu;
  36.  
  37.         string menuId = telerikMenu.ID;
  38.         string containerId = menuId + "_Panel";
  39.         Panel placeHolder = new Panel();
  40.         placeHolder.ID = containerId;
  41.         int originalIndex = FindParentIndex(telerikMenu);
  42.  
  43.         Control parentControl = telerikMenu.Parent;
  44.         parentControl.Controls.Remove(telerikMenu);
  45.         parentControl.Controls.AddAt(originalIndex, placeHolder);
  46.         frm.Controls.Add(telerikMenu);
  47.         telerikMenu.Style["display"] = "none";
  48.  
  49.         SiteMapDataSource ds = new SiteMapDataSource();
  50.         ds.Provider = ISESiteMapProviderFactory.Instance.GetProvider(ThisCustomer.LocaleSetting);
  51.         telerikMenu.DataSource = ds;
  52.         telerikMenu.DataBind();
  53.  
  54.         script.AppendFormat("        var menu_{0} = document.getElementById('{0}');\n", menuId);
  55.         script.AppendFormat("        var panel_{0} = document.getElementById('{0}');\n", containerId);
  56.         script.AppendFormat("        if(menu_{0} && panel_{1}) {{\n", menuId, containerId);
  57.         script.AppendFormat("            panel_{1}.appendChild(menu_{0});\n", menuId, containerId);
  58.         script.AppendFormat("            menu_{0}.style.display = '';\n", menuId, containerId);
  59.         script.AppendFormat("        }}\n");
  60.     }
  61.     script.AppendFormat("    }}\n");
  62.     script.AppendFormat("    $add_windowLoad(loadMenu_{0});\n", randomId);
  63.     script.AppendFormat("</script>\n");
  64.  
  65.     Page.ClientScript.RegisterStartupScript(this.GetType(), randomId, script.ToString());
  66. }

Change the OnPreRender method (around 396) from SetupMenu() To SetupTelerikMenu().  If you ever wish to undo these changes, this is really the only line that you need to change back.

Changing Your Template

In your template.ascx for your skin (\Skins\Skin_1 is the default), add the following with the other Registers at the top (line 3 or 4 usually):

  1. <%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>

Replace everything in the “horizNav” div with the following:

  1. <!-- TOP MENU -->                                                
  2. <telerik:RadMenu runat="server" ID="aspnetMenu" Skin="TTX" EnableEmbeddedSkins="false" />
  3. <!-- END TOP MENU -->

If you want to use a default Telerik skin, replace TTX with the name of the skin you want to use and change EnableEmbeddedSkins to true.

For a custom skin, replace ‘TTX’ with the name of your custom CSS skin for the menu and add the required CSS entries such as:

  1. /*
  2.     Highligh color = D94A46 (Red)
  3.     Font Color = AAAAAA (darker gray)
  4.     Background color = F8F8F8 (light silver)
  5. */
  6.     .RadMenu_TTX
  7.     {
  8.         text-align: left;
  9.         margin: 0px;
  10.         padding: 2px;
  11.     }
  12. /* Root */
  13.     .RadMenu_TTX .rmLink
  14.     {
  15.         line-height: 24px;
  16.         color: Black;
  17.         position: relative;
  18.         display: inline-block !important;
  19.     }
  20.     .RadMenu_TTX .rmText
  21.     {
  22.         color: #AAA;  
  23.         text-decoration: none;
  24.         font-weight: bold;
  25.         font-size: 12pt;    
  26.     }
  27.     .RadMenu_TTX .rmLink:hover
  28.     {
  29.     }
  30.     .RadMenu_TTX .rmFocused,
  31.     .RadMenu_TTX .rmExpanded,
  32.     .RadMenu_TTX .rmText:hover
  33.     {
  34.         color: #D94A46;     
  35.     }
  36.  
  37. /* Sub Menus */
  38.     .RadMenu_TTX .rmGroup
  39.     {
  40.         border: 1px solid #AAA;
  41.     }
  42.     .RadMenu_TTX .rmGroup .rmLink
  43.     {
  44.         background: White;
  45.     }
  46.     .RadMenu_TTX .rmGroup .rmFocused,
  47.     .RadMenu_TTX .rmGroup .rmExpanded,
  48.     .RadMenu_TTX .rmGroup .rmText
  49.     {
  50.         background: White;
  51.         font-weight: normal;
  52.         font-size: 10pt;    
  53.         color: #AAA;
  54.     }
  55.     .RadMenu_TTX .rmGroup .rmLink:hover
  56.     {
  57.         background: #D94A46;
  58.     }
  59.     .RadMenu_TTX .rmGroup .rmText:hover
  60.     {
  61.         color: #D94A46;     
  62.     }

And you should be done.  All-in-all, it will take you longer to get your CSS file right than it will to do the ‘hard stuff’

Tags: , , ,

Blog

About the author

I am a software developer working for Microsoft in Redmond, WA.  In addition, my wife and I own TTXOnline, what is likely the 3rd largest table tennis store in the US.

Month List

Page List