Modify the DNN Search Button

Recently, I have a chance to work with DNN skin and containers using PhotoShop files from a designer. So far so good. However, with the latest skin, an image must be used instead of text for the DNN search button (<dnn:SEARCH> control).
First, create a copy of the search user control located under admin/skins and name it "CustomSearch.ascx". Then modify the source code to set custom css classes for the controls inside. The modified code looks something like this:
<%@ Control language="vb" CodeFile="Search.ascx.vb" AutoEventWireup="false" Explicit="True" Inherits="DotNetNuke.UI.Skins.Controls.Search" %>
<asp:RadioButton ID="optWeb" runat="server" CssClass="SkinObject" GroupName="Search" Text="Web" />
<asp:RadioButton ID="optSite" runat="server" CssClass="SkinObject" GroupName="Search" Text="Site" />
<asp:TextBox id="txtSearch" runat="server" CssClass="SearchTextbox" enableviewstate="False" MaxLength="255"></asp:TextBox>
<asp:LinkButton ID="cmdSearch" Runat="server" CausesValidation="False" CssClass="SearchButton"></asp:LinkButton>
Note that we could have modified the existing Search.ascx but that might affect the whole site since some skins might make use of this default search e.g. the default blue skin.
Notice that it uses a link button as a search button. This will be converted into an <a> HTML tag and thus cannot set an image for it. We need to set it in CSS 9not shown here).
Also notice that the code file is still "Search.ascx.vb". Therefore, I did not change any control's ID just for simplicity and compatibility with the code file. I also assigned "SearchTextbox" and "SearchButton" CSS classes for <asp:Textbox id="txtSearch"> and <asp:LinkButton id="cmdSearch"> accordingly. These CSS classes have been defined in my skin.css under my skin folder. The image for the button has also been set here as a background image.
Now place the CustomSearch.ascx back to admin/skins folder and use <dnn:CUSTOMSEARCH> instead of <dnn:SEARCH> in my skin ascx file. This ALMOST gave me the result I expected. The textbox and image button shows properly BUT there is a text "search" appearing on the search button.
After some investigations in the Search.ascx.vb file, I found that the search text is extracted from a resource file.
In the "Private Members" region, a file name is set as a constant here.
Const MyFileName As String = "Search.ascx"
Under the "Event Handlers" region, a string value has been extracted from a resource file and set as the search text.
Submit = Services.Localization.Localization.GetString("Search", Services.Localization.Localization.GetResourceFile(Me, MyFileName))
.
.
.
cmdSearch.Text = Submit
Now look for "Search.ascx.resx" file under App_LocalResources folder. This file is the resource file that the code calls. Look near the bottom of the file and we see...
<data name="Search.Text"> <value>Search</value> </data>
Bingo! The text that we DON'T want is here. We could just remove the string value but, again, it might affect other skins. So I decided to create another name-value pair and set no value to it.
<data name="SearchNoText.Text"> <value></value> </data>
We could have created another resource file but I don't think it's necessary since adding another name-value pair should work just fine. We will discuss more on this issue in a moment.
Go back to the Search.ascx.vb file and change the line where it gets the string resource to the following:
Submit = Services.Localization.Localization.GetString("SearchNoText", Services.Localization.Localization.GetResourceFile(Me, MyFileName))
.
.
.
cmdSearch.Text = Submit
Save the file as CustomSearch.ascx.vb. Now we have got the code file we want which calls a string value we are looking for (which is a blank). What to do next is to tell the skin to use this code file instead.
<%@ Control language="vb" CodeFile="CustomSearch.ascx.vb" AutoEventWireup="false" Explicit="True" Inherits="DotNetNuke.UI.Skins.Controls.Search" %>
<asp:RadioButton ID="optWeb" runat="server" CssClass="SkinObject" GroupName="Search" Text="Web" />
<asp:RadioButton ID="optSite" runat="server" CssClass="SkinObject" GroupName="Search" Text="Site" />
<asp:TextBox id="txtSearch" runat="server" CssClass="searchtextbox" enableviewstate="False" MaxLength="255"></asp:TextBox>
<asp:LinkButton ID="cmdSearch" Runat="server" CausesValidation="False" CssClass="searchbutton"></asp:LinkButton>
Now put this skin in use and the search text will no longer be there :)
In conclusion, here is how it gets to the resource string:
CustomSearch.ascx (skin) --> CustomSearch.ascx.vb (code file) --> Search.ascx.resx.
We could have created a another resource file as mentioned earlier. For example, we could create a copy of Search.ascx.resx and name it "CustomSearch.ascx.resx". In this file, we can just remove the value for the data name "Search.Text". The rest remains unchanged. Then in the code file, change the MyFileName constant to "CustomSearch.ascx". This way, the code file will access CustomSearch.ascx.resx to look for the resource string. But, again, there is no need to do that much. Just add another name-value pair and we are ready to go :)
I hope this could give you an idea on how to modify the search module to look the way you want :)
Further Study:
It might be possible to use an image button for the search button and set the id to be the same (cmdSearch). This could also be used to customize the search behavior of the search module. By the way, I am a C# developer, not VB.NET. So, later I will try to create some with C# :)

DNN Tokens

Token

Description

[Portal:Currency]

Currency String

[Portal:Description]

Portal Description

[Portal:Email]

Portal Admin Email

[Portal:FooterText]

Portal Copyright Text

[Portal:HomeDirectory]

Portal (relative) Path of Home Directory

[Portal:LogoFile]

Portal Path to Logo file

[Portal:PortalName]

Portal Name

[Portal:PortalAlias]

Portal URL

[Portal:TimeZoneOffset]

Difference in minutes between Portal default time and UTC

 

 

[User:DisplayName]

User’s Display Name

[User:Email]

User’s Email Address

[User:FirstName]

User’s First Name

[User:FullName]

[deprecated]

[User:LastName]

User’s Last Name

[User:Username]

User’s Login User Name

 

 

[Membership:Approved]

Is User Approved?

[Membership:CreatedDate]

User Signup Date

[Membership:IsOnline]

Is User Currently Online?

 

 

[Profile:property]

Use any default or custom Property defined for user profiles as listed in Profile Property Definition section of Manage User Accounts. Please use non-localized Property titles only.

 

 

[Tab:Description]

Page Description Text for Search Engine

[Tab:EndDate]

Page Display Until Date

[Tab:FullUrl]

Page Full URL

[Tab:IconFile]

Page Relative Path to Icon file

[Tab:KeyWords]

Page Keywords for Search Engine

[Tab:PageHeadText]

Page Header Text

[Tab:StartDate]

Page Display from Date

[Tab:TabName]

Page Name

[Tab:TabPath]

Page Relative Path

[Tab:Title]

Page Title (Window Title)

[Tab:URL]

Page URL

 

 

[Module:Description]

Module Definition Description

[Module:EndDate]

Module Display Until Date

[Module:Footer]

Module Footer Text

[Module:FriendlyName]

Module Definition Name

[Module:Header]

Module Header Text

[Module:HelpUrl]

Module Help URL

[Module:IconFile]

Module Path to Icon File

[Module:ModuleTitle]

Module Title

[Module:PaneName]

Module Name of Pane where UDT resides

[Module:StartDate]

Module Display from Date

 

 

[DateTime:Now]

Current Date and Time

[Ticks:Now]

CPU Tick Count for Current Second

[Ticks:Today]

CPU Tick Count since Midnight

[Ticks:TicksPerDay]

CPU Ticks per Day (for calculations)

For date/time and numeric values, you can also append a "format" string defined by the .Net framework, for example: [DateTime:Now|"format"] current date/time formatted according to "format", e. g. [DateTime:Now|f] displays current date in short format (does not apply to expressions of calculated columns)

The Mystery of XmlNode.SelectNodes()

With the xml document found here and the following code, what will you get as the result?
Shortened version of the XML document:
    <Items>
        <ItemAttributes>
            <ListPrice>
                <FormattedPrice>$49.00</FormattedPrice>
            </ListPrice>
        </ItemAttributes>
        <OfferSummary>
            <LowestNewPrice>
                <FormattedPrice>$29.99</FormattedPrice>
            </LowestNewPrice>
            <LowestUsedPrice>
                <FormattedPrice>$24.99</FormattedPrice>
            </LowestUsedPrice>
        </OfferSummary>
        <Offers>
            <Offer>
                <FormattedPrice>$..(1)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(2)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(3)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(4)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(5)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(6)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(7)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(8)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(9)..</FormattedPrice>
            </Offer>
            <Offer>
                <FormattedPrice>$..(10)..</FormattedPrice>
            </Offer>
        </Offers>
    </Items>
</ItemLookupResponse>
XmlDocument xDoc = new XmlDocument();
xDoc.Load("resources/ItemLookupResponse.xml");
XmlNamespaceManager nsMgr = new XmlNamespaceManager(xDoc.NameTable);
nsMgr.AddNamespace("aws", xDoc.DocumentElement.NamespaceURI);
XmlNode offersNode = xDoc.SelectSingleNode("//aws:Offers[1]", nsMgr);
XmlNodeList formattedPriceNodes = offersNode.SelectNodes("//aws:FormattedPrice", nsMgr);
return formattedPriceNodes.Count;
Since formattedPriceNodes selects nodes under offersNode, I expected it to return 10.
But it returns 13!!!
That means it parses the whole document (xDoc), not just the node (offersNode).
The problem seems to be on the XPath expression.
Using // will parse the whole document.
To get all the matched descendant nodes, use ".//" or "descendant::".
Change the line with //aws:FormattedPrice to be as followed:
XmlNodeList formattedPriceNodes = offersNode.SelectNodes("descendant::aws:FormattedPrice", nsMgr);
The result is now 10 as expected. :D

Enable an iterator for a class

To enable an iterator for a class, simply add GetEnumerator() method which return IEnumerator interface to that class.
Note that you'll need a list-type member (Array, ArrayList, LinkedList,...) that you want to loop through in your list.
Within that method, loop through the list and return each item on each loop.
Example:
#region VideoGame item public struct VideoGame{ private string asin; private string title; public string ASIN{ set{ asin = value; } get{ return asin; } } public string Title{ set{ title = value; } get{ return title; } } } #endregion #region VideoGame collection class public class VideoGameCollection{ private LinkedList m_VideoGameLinkedList = new LinkedList(); public VideoGameCollection{ // Default constructor. } public int Count{ get{ return m_VideoGameLinkedList.Count; } } public void Add(VideoGame vdoGame){ m_VideoGameLinkedList.AddLast(vdoGame); }
public IEnumerator GetEnumerator(){ foreach(VideoGame game in m_VideoGameLinkedList){ yield return game; }     }
 }  #endregion  #region Loop through VideoGameCollection in your main class ... VideoGameCollection games = new VideoGameCollection(); foreach(VideoGame game in games){ Console.WriteLine( game.Title ); } ...  #endregion
The colored lines are the key to enable "foreach" function to your class.
Please note that this requires System.Collections namespace.
Note that for the GetEnumerator() method, just return m_VideoGameLinedList.GetEnumerator(); should do the job.
I will get back again for an update.

Customize DataRow to keep an object

To enable a DataRow to keep a custom object, create a new class inheriting DataRow class.
The code below shows how to keep an object of a custom class named "VideoGame".
 
#region Custom DataRow
pubilc class VideoGameDataRow : DataRow{
        private VideoGame vdoGame;
        public VideoGameDataRow(DataRowBuilder rowBuilder) : base(rowBuilder)
                // Default constructor.
        }
        public VideoGame GameObject{
                set { vdoGame = value; }
                get { return vdoGame; }
        }
}
#endregion
 
You will also need a custom DataTable to handle this custom DataRow since the default DataTable can take only the default DataRow.
Override the NewRow method (NewRowBuilder()) to handle the custom DataRow.
 
#region Custom DataTable
public class VideoGameTable : DataTable{
        protected override DataRow NewRowBuilder(DataRowBuilder builder){
                return new VideoGameDataRow(builder);
        }
}
#endregion
 
When used, cast a created row as the custom DataRow.
 
#region Usage
...
        VideoGame game = new VideoGame();
        VideoGameDataTable vdoGameTable = new VideoGameDataTable();
        VideoGameDataRow newGameRow = (VideoGameDataRow)(vdoGameTable.NewRow());
        newGameRow.GameObject = game;
...
#endregion
 
Note that you cannot create a new DataRow using the new keyword (DataRow row = new DataRow();).
This will generate an error saying that DataRow(DataRowBuilder) is inaccessible due to its protection level.
This applies to a custom DataRow which inherits from DataRow as well.