Visar inlägg med etikett episerver. Visa alla inlägg
Visar inlägg med etikett episerver. Visa alla inlägg

torsdag 4 februari 2016

Sätta default value till nyskapat content innan det publicerats

Ett litet tips:

För att sätta default värden på en content typ i EPiServer gör man ju en override på SetDefaultValues() metoden i sin contenttype deklarationsklass.

Men hur sätter  man default värde på ContentReference egenskaper när man vill peka ut den egna sidan? SetDefaultValues anropas ju innan sidan/blocket ens har tilldelats ett ContentReference-värde.

Jo, svaret stavas ContentReferense.SelfReference !!

public override void SetDefaultValues(ContentType contentType)
{
   base.SetDefaultValues(contentType);

   // Default on local property!
   this.MyContentReferenceProperty = ContentReference.SelfReference;
           
   // Also works for properties on local blocks!
   this.MyLocalBlock.ContentReferenceProperty = ContentReference.SelfReference;

}


Lätt som en plätt!

tisdag 2 februari 2016

Sätta behörigheter programmatiskt i EPiServer 8

Tjo!

Long time no see. Råkade skaffa 2 barn så har inte haft tid att skriva nåt på ett tag.

Eftersom jag inte hittade något användbart själv när jag googlade på behörighetsfipplande i EPiServer 8 så tänkte jag att jag drar mitt strå till stacken och lägger upp ett inlägg själv:

Scenariot är att när en egenskap som pekar ut en användare ändras på en EPiServer-sida så ska EPiServer-sidans behörigheter automatiskt ändras. Detta görs genom en initialiseringsmodul som hookar upp på Published-eventet. Men detta är en annan historia...

För att uppnå mina behov behövde jag två metoder, en som tar bort behörigheter från en sida och en metod som lägger till behörigheter.

Detta görs numera bäst med hjälp av servicen IContentSecurityRepository som nås via ”ServiceLocator”-klassen (fick det aldrig att fungera genom att manipulera PageData.ACL...).

private IContentSecurityRepository _contentSecurityRepository
{
  get
  {
    return ServiceLocator.Current.GetInstance<IContentSecurityRepository>();
  }
}

Ta bort behörighet
En metod som kollar om den utpekade sidan har någon entry för den specificerade rollen eller användaren. Om så tas entryn bort. En borttagning sker lite klurigt genom att man lägger till en entry med “NoAccess”-behörigheten och skriver över befintliga regler (mha parametern SecuritySaveType.Replace i save-metoden).
        
/// <summary>
/// Remove ACL entry for specified user and accessright for specified page.
/// </summary>
/// <param name="userOrRoleName"></param>
/// <param name="securityEntityType"></param>
/// <param name="page"></param>
public void RemoveAccessRight(string userOrRoleName, SecurityEntityType securityEntityType, PageData page, bool keepInheritedSecurity)
{
  // Check entry exists
  if (page.ACL.Contains(userOrRoleName))
  {
    IContentSecurityDescriptor securityDescriptor = (IContentSecurityDescriptor)_contentSecurityRepository.Get(page.ContentLink).CreateWritableClone();

    // Remove inherited if set (will throw error otherwise)
    if (securityDescriptor.IsInherited)
    {
      securityDescriptor.IsInherited = false;

      // Keep inherited security?
      if (keepInheritedSecurity)
        AddInheritedACLEntries(ref securityDescriptor, page.ParentLink);
    }

    // Remove by adding an entry with NoAccess and replace existing one (achieved with SecuritySaveType argument to Save method)
    securityDescriptor.AddEntry(new AccessControlEntry(userOrRoleName, AccessLevel.NoAccess, securityEntityType));
    _contentSecurityRepository.Save(page.ContentLink, securityDescriptor, SecuritySaveType.Replace);
  }
}

Manuellt lägga till ärvda behörigheter
Dock så finns risken att sidans behörigheter är inställda på att ärva behörigheter från föräldersidan. Denna flagga ” IsInherited” på IContentSecurityDescriptor-objektet medför att vi får ett exception kastat om vi försöker spara nya behörigheter. Så vi måste först för säkerhets skull sätta “IsInherited” till false. För att då inte tappa alla behörigheter som arvet medförde så har jag lagt till en parameter ”keepInheritedSecurity” som vid true manuellt lägger till de ärvda behörigheterna mha metoden ”AddInheritedACLEntries” nedan.

/// <summary>
/// Adds the inherited ACL entries as new entries on specified security descriptor object
/// </summary>
/// <param name="securityDescriptor"></param>
/// <param name="parentLink"></param>
private void AddInheritedACLEntries(ref IContentSecurityDescriptor securityDescriptor, ContentReference parentLink)
{
  if (!ContentReference.IsNullOrEmpty(parentLink))
  {
    // Read parent ACL entries and add to security descriptor
    PageData parent = _contentLoader.Get<PageData>(parentLink);
    foreach (var aclEntry in parent.ACL.Entries)
    {
      securityDescriptor.AddEntry(aclEntry);
    }
  }
}


Lägga till behörighet
Slutligen en metod för att lägga till en behörighet som är ganska self explanatory. Samma sak här angående flaggan ”IsInherited”.

Tänk också på att AccessLevel enum är en bitmask. Den kan alltså innehålla flera värden samtidigt, t.ex. kan man skriva såhär för att ge en variabel alla värden utom ”Administer”:

AccessLevel accessLevel = AccessLevel.Read | AccessLevel.Create | AccessLevel.Edit | AccessLevel.Delete | AccessLevel.Publish;




Och här är metoden för att lägga till en behörighet:

/// <summary>
/// Add ACL entry for specified user or role with specified level on specified page.
/// </summary>
/// <param name="userOrRoleName"></param>
/// <param name="accessLevel"></param>
/// <param name="securityEntityType"></param>
/// <param name="page"></param>
/// <param name="keepInheritedSecurity"></param>
public void AddAccessRight(string userOrRoleName, AccessLevel accessLevel, SecurityEntityType securityEntityType, PageData page, bool keepInheritedSecurity)
{
  if (page != null && !page.ACL.Contains(userOrRoleName, accessLevel, securityEntityType))
  {
    IContentSecurityDescriptor securityDescriptor = (IContentSecurityDescriptor)_contentSecurityRepository.Get(page.ContentLink).CreateWritableClone();

    // Remove inherited if set (will throw error otherwise)
    if (securityDescriptor.IsInherited)
    {
      securityDescriptor.IsInherited = false;

      // Keep inherited security?
      if (keepInheritedSecurity)
        AddInheritedACLEntries(ref securityDescriptor, page.ParentLink);
    }
                
    securityDescriptor.AddEntry(new AccessControlEntry(userOrRoleName, accessLevel, securityEntityType));
    _contentSecurityRepository.Save(page.ContentLink, securityDescriptor, SecuritySaveType.Replace);
  }

}


Och det var det!
Dessa metoder lade jag sedan i en egen klass för behörighetsfippel.

tisdag 28 maj 2013

Lagra custom property sträng som binär protobuf-net sträng istället för xml

Hej!

Som en reaktion på mitt föregående inlägg bestämde jag mig för att kika på alternativ för serialisering/deserialisering. Kom över följande "benchmark" (vetenskapligheten i inlägget orkar jag inte analysera).

http://stackoverflow.com/questions/3790728/performance-tests-of-serializations-used-by-wcf-bindings/3793091#3793091

Sagt och gjort, protobuf-net it is.

1. Börjar med att tanka ner dll från https://code.google.com/p/protobuf-net/

2. Följer instruktionerna och dekorerar min databärande klass med attributen [ProtoContract] & [ProtoMember(x)] (https://code.google.com/p/protobuf-net/wiki/GettingStarted)















3. Implementerar ToString() override och den statiska Parse-metoden som min custom property använder när objektet skrivs och läses:














4. Som synes använder jag mig av en statisk helper-klass för ändamålet:



5. Och nu ska det alltså bara vara att tuta å köra! Mindre dret i databasen ist krieg! Medger dock att läsbarheten försämras rejält vid t.ex. hantering av olika objekt-versioner. Men det är ett senare problem! :)






fredag 24 maj 2013

EPiServer Custom Property som lagras som longstring och stringDelayedLoadThreshold

Hej igen. Nej bloggen är inte död, den har bara idlat lite.

Stötte på en irriterande detalj i EPiServer 7 när jag lekte med en custom property (använder legacyeditorn tills jag orkat sätta mig in i Dojo-tänket) vars Data serialiserades till en sträng och lagrades i EPiServers db som en LongString.

Egenskapen fungerade utmärkt i redigeraläget och efter publicering av en sida. Ända tills en IISRESET av sidan gjordes. Då returnerade propertyn alltid null. Troligtvis nåt i cache-mekanismen som strular alltså.

Det visade sig härröra från ett filter för longstrings-egenskaper vars värden är för långa. Lite osäker på vad defaultvärdet är, men när jag satte attributet stringDelayedLoadThreshold="0" för noden siteSettings i EPiServer.config så försvann problemet. Drygt fel...

Länk till var jag hittade lösningen:
http://world.episerver.com/Modules/Forum/Pages/thread.aspx?id=65572&pageIndex=1#reply

Dokumentation för egenskapen:
http://world.episerver.com/Documentation/Class-library/?documentId=cms/7/89421fe0-ae9b-42fe-06d0-224bd9e4a528




torsdag 15 november 2012

EPiServer Powershell deploy script

Tjipp! som Bengt Alsterlind säger här i Värmland (iaf inte hört någon annan använda ordet).

Blivit mycket powershell för min del på sistone. Men måste säga att jag gillar språket mer och mer, ett klart fall framåt från gamla dos-batch. Här kommer ytterligare lite powershell:

Tänkte presentera ett litet förslag på hur ett EPiServer powershell deploy script kan se ut. Alltså ett script som kopierar visst innehåll från en Visual Studio projekt-katalog till en annan katalog i syfte att kunna zippa ihop innehållet och flänga upp på en testserver/prod.server. Har läst att det ska finnas färdiga såna här templates i EPiServer 7 (vet inte om det är MSBuild eller PS), men vet inte mer om detta just nu.

<Dicsclaimer>Observera att alla webbplatser har sina egna krav och behov och att detta script antagligen inte överensstämmer med just dina krav.</Disclaimer> :P


# DeployPackage.ps1
# 2012-11-14 Määts

# Example call:
# ./DeployPackage.ps1 -verbose -c "Release" -s "C:\Path\To\My\EPiServer\Project\" -d "C:\Path\To\My\Deploy\Folder\"

# Script params
param([switch]$verbose, [string]$configurationMode, [string]$sourceDir, [string]$destDir)

$verboseoutput = @"
Executing script with params:
configurationMode: $configurationMode
sourceDir:         $sourceDir
destDir:           $destDir
"@

# Handle switches
if ($verbose.IsPresent) {
    Write-Host $verboseoutput
    $VerbosePreference = "Continue"
} else {
    $VerbosePreference = "SilentlyContinue"
}
# Validate params
if (-NOT($sourceDir -eq $NULL) -and (Test-Path $sourceDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""sourceDir"" does not appear to be a valid path... aborting."
    Write-Host $sourceDir
    break
}

if (-NOT($destDir -eq $NULL) -and (Test-Path $destDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""destDir"" does not appear to be a valid path... aborting."
    Write-Host $destDir
    break
}
if ($configurationMode -eq $NULL)
{
    Write-Host "ERROR: Parameter ""configurationMode"" not set... aborting."
    break
}
if (([string]$configurationMode).ToLower() -ne "release") {
    Write-Host "INFO: Parameter ""configurationMode"" not set to release mode... aborting."
    break
}

# Declare function
function DeployPackage() {
<#
    .SYNOPSIS
    Creates an EPiServer site deployment package
   
    .DESCRIPTION
   
    .PARAMETER
   
    .EXAMPLE
   
    .INPUTS
   
    .OUTPUTS
#>

    # Function params
    param([string]$configurationMode, [string]$sourceDir, [string]$destDir)
       
    # Variables
    $dateString = (Get-Date -format "yyyy-MM-dd_HH.mm.ss") # String representation of current date that will be used when naming the deploy package
    $fileTypes = @("*.aspx","*.ascx","*.jpg","*.gif","*.png","*.asmx","*.resx","*.xml","*.jpg","*.js","*.css","*.dll",
                   "*.pdb","*.swf","*.txt","*.html","*.htm","*README*","*.php","*.pdn","*.master","*.xsd","*.fla",
                   "*.as","*.asax") # This is the filetypes that the script will copy into the deploy
    $excludeFolders = @("obj")
    $excludeFiles = @("EPiServerErrorLog.txt")
                 
    $newDestDir = "$destDir\DeployPackage_$dateString\"
   
    # Create new folder
    New-Item $newDestDir -type directory
 
    #Get all files to copy
    $filesToCopy = Get-ChildItem $sourceDir -recurse -include $fileTypes
   
    # Copy items
    foreach ($file in $filesToCopy)
    {
        $newFilePath = $file.Fullname.Replace($sourceDir,$newDestDir)
       
        # Create new file (unix touch) in order to maintain folder structure
        New-Item -ItemType File -Path $newFilePath -Force
       
        # Copy file (overwrite)
        Copy-Item $file.Fullname $newFilePath -Force
    }
   
    # Remove unwanted folders
    Get-ChildItem $newDestDir -include $excludeFolders -recurse | Where-Object { $_.PSIsContainer } | Foreach-Object { Remove-Item $_.Fullname -recurse -Force }
   
    # Remove unwanted files
    Get-ChildItem $newDestDir -recurse -include $excludeFiles -Force | Foreach-Object { Remove-Item $_.Fullname }
}

# Call function
DeployPackage -c $configurationMode -s $sourceDir -d $destDir


Kommentar:
För att köra scriptet slänger man in följande i post-build events i Visual Studio för EPiServer webb-projektet.

 Powershell.exe -file "$(SolutionDir)DeployPackage.ps1" -ExecutionPolicy Unrestricted -verbose -c "$(ConfigurationName)" -d "C:\Deploy" -s "$(ProjectDir)\"

 Mitt script ligger alltså i "Solutiondir"-roten. Jag skickar in $(ConfigurationName) eftersom jag enbart vill att en deploy mapp ska skapas när projektet byggs i release-mode. Slutligen skickar jag med aktuell projektkatalog och vart jag vill att deploy-mappen ska skapas.

Som synes har jag slarvat lite i scriptet och inte orkat skriva klart kommentarer. Erkänner också villigt att #Remove unwanted files/folders inte är speciellt snyggt utan hade kunnat filtrerats redan i kopieringsstadiet. Det hade också varit snyggt om jag kunde avslutat med att zippa ihop mappen men detta är inte sååå jobbigt att göra själv jämfört med att sitta och hantera alla filtyper osv. 

Om nån läsare har kritik/bättre idéer så är jag idel öra, är som sagt fortfarande grön i powershell träsket :)

måndag 15 oktober 2012

Inline-script i editor på EPiServer CMS 5

Hej!

Stötte nyligen på ett problem där en redaktör ville kunna lyfta in tredjeparts javascript i en editor i EPiServer CMS 5 R2 via html-läget.

Tydligen så verkar det fungera att göra det, det sparas ned korrekt till sidan. Dock försvinner alla inmatade värden i Editorn när redaktören redigerar egenskapen på nytt.

För att gå runt detta kan man mata in följande, t.ex.:
<P>&nbsp;

<SCRIPT src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></SCRIPT></P>

Så stannar det kvar nästa gång man editerar! Gött!

Obs. fulhetsdisclaimer: Den wrappade <p>-taggen är det EPiServer som lägger in och inte jag.

måndag 30 juli 2012

Gruppera sidtyper i adminläget på EPiServer CMS 5

Hej igen, åter från semester och tänkte slänga upp lite fulkod som grupperar sidtyperna i adminläget på EPiServer CMS 5.

Jag har nämligen upplevt att när man bygger en stor EPiServer-site med många sidtyper, så blir det ofta rörigt att bläddra bland sidtyper i adminläget. Eftersom så gott som samtliga projekt jag varit med i följt namnsättningsstandarden som EPiServer själva följer i sitt templates-projekt, med prefixet [Public] osv., så kändes gruppering på prefix som den logiska vägen att gå.

Obs. på att följande lösning enbart är testad i EPiServer CMS 5 R2 SP2 (5.2.375.236)

Hittar ni några fel eller andra dômmheter så hojta gärna till!

Steg 1, hitta & öppna aspx-sida som sköter EPiServers adminläge-meny.

Den återfinns under EPiServers installationsmapp, t.ex. "C:\Program Files\EPiServer\CMS\5.2.375.236\Application\UI\Admin\" och heter menu.aspx. Börja med att skapa en backup av filen.

Steg 2, lägga in javascript för gruppering

Öppna sedan filen i din favoriteditor, leta upp raden:


EPi.AddEventListener(window, "load", EPiCheckSortOrder);

och släng in följande javascript efteråt:


/* Change grouping behavior to selected one */
function changeGrouping(grouping) {
    var pageTypeTreeGrouping = document.getElementById('PageTypeTreeGrouping');

    if (!window.top.epiMenuGrouping)
        window.top.epiMenuGrouping = null;

    // Already toggled, untoggle
    if (window.top.epiMenuGrouping == grouping) {
        pageTypeTreeGrouping.className = 'EPEdit-CommandTool';
        window.top.epiMenuGrouping = null;
        UnGroupPageTypesByPrefix();
    } else {
        switch (grouping) {
            case 'prefix':
                pageTypeTreeGrouping.className = 'EPEdit-CommandToolClicked';
                GroupPageTypesByPrefix();
                break;
        } // Add more grouping alternatives when implemented...
        window.top.epiMenuGrouping = grouping;
    }
}

/* For browser compability (IE) */
var getElementsByClassName = function(oNode, class_name) {
    var docList = oNode.all || oNode.getElementsByTagName('*'),
            matchArray = new Array(),
            re = new RegExp("(?:^|\\s)" + class_name + "(?:\\s|$)"),
            i = 0,
            docListLength = docList.length;

    for (i = 0; i < docListLength; i = i + 1) {
        if (re.test(docList[i].className)) {
            matchArray[matchArray.length] = docList[i];
        }
    }
    return matchArray;
};

/* Toggles selected group visibility */
function toggleGroup() {
    var oParent = this.parentElement,
            oToggle = getElementsByClassName(oParent, 'toggle'),
            oExpandIcon = getElementsByClassName(oParent, 'expandIcon');
    if (!!oToggle && oToggle.length > 0 && !!oExpandIcon && oExpandIcon.length > 0) {
        oToggle = oToggle[0];
        var toggleVisible = oToggle.style.display == 'none';
        oToggle.style.display = (toggleVisible ? '' : 'none');
        oExpandIcon = oExpandIcon[0];
        oExpandIcon.style.backgroundImage = (toggleVisible ? 'url(../../App_Themes/Default/Images/ExplorerTree/minus.gif)' : 'url(../../App_Themes/Default/Images/ExplorerTree/plus.gif)');
    }
    return false;
};

/* Ungroups the page types and restores them into default behavior */
function UnGroupPageTypesByPrefix() {
    var oListDiv = document.getElementById('PageTypeListDiv'),
            oMenuItems = getElementsByClassName(oListDiv, 'EPAdmin-menuItem'),
            clonedMenuItems = [],
            i = 0;

    for (i; i < oMenuItems.length; i = i + 1) {
        clonedMenuItems.push(oMenuItems[i].cloneNode(true));
    }
    oListDiv.innerHTML = '';
    for (i = 0; i < clonedMenuItems.length; i = i + 1) {
        oListDiv.appendChild(clonedMenuItems[i]);
    }
};

/* Groups the page types depending on their prefix (eg. [Public] ... */
function GroupPageTypesByPrefix() {
    var oListDiv = document.getElementById('PageTypeListDiv'),
            oPageTypeLinks = getElementsByClassName(oListDiv, 'pageTypeLink'),
            linkGroups = [],
            i = 0;

    /* Get prefixes */
    for (i; i < oPageTypeLinks.length; i = i + 1) {
        var pageTypeName = oPageTypeLinks[i].innerText.replace(new RegExp(" ", "g"), '').replace(/(\r\n|\n|\r)/gm, ""),
                pageTypePrefixes = pageTypeName.match("\\[[^\\]]*]"),
                pageTypePrefix = (!!pageTypePrefixes && pageTypePrefixes.length > 0) ? pageTypePrefixes[0] : "";

        if (!!!pageTypePrefix) pageTypePrefix = '[ ]';

        if (!!!linkGroups[pageTypePrefix]) {
            linkGroups.push(pageTypePrefix);
            linkGroups[pageTypePrefix] = [oPageTypeLinks[i].cloneNode(true)];
        } else {
            linkGroups[pageTypePrefix].push(oPageTypeLinks[i].cloneNode(true));
        }
    }

    oListDiv.innerHTML = '';

    /* Create prefix group holders */
    var oDiv,
            oInnerDiv,
            j = 0,
            expandIcon,
            groupTitle = '',
            oClearDiv;

    for (i = 0; i < linkGroups.length; i = i + 1) {
        oDiv = document.createElement('div');
        oDiv.className = 'group';
        oDiv.style.marginTop = '5px';
        oListDiv.appendChild(oDiv);

        expandIcon = document.createElement('span');
        expandIcon.className = 'expandIcon';
        expandIcon.style.backgroundImage = 'url(../../App_Themes/Default/Images/ExplorerTree/plus.gif)';
        expandIcon.style.width = '15px';
        expandIcon.style.height = '18px';
        expandIcon.style.position = 'absolute';
        expandIcon.style.cursor = 'pointer';
        expandIcon.style.backgroundPosition = 'left center';
        expandIcon.style.backgroundRepeat = 'no-repeat';
        expandIcon.style.backgroundColor = '#EEEEEE';
        expandIcon.onclick = groupTitle.onclick = toggleGroup;
        expandIcon.innerHtml = '&nbsp;';
        oDiv.appendChild(expandIcon);

        groupTitle = document.createElement('span');
        groupTitle.innerText = linkGroups[i];
        groupTitle.className = 'groupTitle';
        groupTitle.style.marginLeft = "20px";
        groupTitle.style.fontWeight = "bold";
        groupTitle.style.cursor = 'pointer';
        groupTitle.onclick = toggleGroup;
        oDiv.appendChild(groupTitle);

        oInnerDiv = document.createElement('div');
        oInnerDiv.style.display = 'none';
        oInnerDiv.style.marginLeft = '25px';
        oInnerDiv.className = 'toggle';
        oDiv.appendChild(oInnerDiv);

        for (j = 0; j < linkGroups[linkGroups[i]].length; j = j + 1) {
            oInnerInnerDiv = document.createElement('div');
            oInnerInnerDiv.className = 'EPAdmin-menuItem';
            oInnerDiv.appendChild(oInnerInnerDiv);
            oInnerInnerDiv.appendChild(linkGroups[linkGroups[i]][j]);
        }

        oClearDiv = document.createElement('div');
        oClearDiv.style.clear = 'both';
        oDiv.appendChild(oClearDiv);
    }
}


Steg 3, modifiera sortering så att grupperingen inte bryts vid sortering. 

Ersätt funktionen "changeSortOrder" med följande:


/* Note: modified */
function changeSortOrder(order) {
    var container = document.getElementById('PageTypeListDiv'),
            groups = getElementsByClassName(container, 'group'),
                                       links,
                                       linkArray = [[]],
                                       sortFunction,
            i = 0,
            j = 0,
            oPageTypeIndexSorting = document.getElementById('PageTypeIndexSorting'),
            oPageTypeAlphabeticalSorting = document.getElementById('PageTypeAlphabeticalSorting');

    // IE has innerText property which other browsers don't
    // why we have to get and set the text in another way.
    function GetInnerText(node) {
        if (node.innerText != null)
            return node.innerText;
        else
            if (node.childNodes.length > 0 && node.childNodes[0].nodeType == 3) // nodeType 3 is textNode.
            return node.childNodes[0].nodeValue;
    }

    function SetInnerText(node, text) {
        if (node.innerText != null)
            node.innerText = text;
        else
            if (node.childNodes.length > 0 && node.childNodes[0].nodeType == 3) // nodeType 3 is textNode.
            return node.childNodes[0].nodeValue = text;
    }

    function CreateLinkObject(oLink) {
        var link = new Object();
        link.innerText = GetInnerText(oLink);
        link.id = oLink.id;
        link.href = oLink.href;
        return link;
    }

    if (!!groups && groups.length > 0) {
        for (i = 0; i < groups.length; i = i + 1) {
            links = getElementsByClassName(groups[i], 'pageTypeLink');
            for (j = 0; j < links.length; j = j + 1) {
                if (!!!linkArray[i]) {
                    linkArray[i] = [];
                }
                linkArray[i].push(CreateLinkObject(links[j]))
            }
        }
    } else {
        links = document.getElementsByName('pLink');
        linkArray[0] = [];
        for (i = 0; i < links.length; i = i + 1) {
            linkArray[0].push(CreateLinkObject(links[i]));
        }
    }

    switch (order) {
        case 'index':
            sortFunction = SortByIndex;
            oPageTypeIndexSorting.className = 'EPEdit-CommandToolClicked';
            oPageTypeIndexSorting.style.cursor = 'default';
            oPageTypeAlphabeticalSorting.className = 'EPEdit-CommandTool';
            oPageTypeAlphabeticalSorting.style.cursor = 'pointer';
            break;
        case 'alphabetical':
            sortFunction = SortByAlphabet;
            oPageTypeAlphabeticalSorting.className = 'EPEdit-CommandToolClicked';
            oPageTypeAlphabeticalSorting.style.cursor = 'default';
            oPageTypeIndexSorting.className = 'EPEdit-CommandTool';
            oPageTypeIndexSorting.style.cursor = 'pointer';
            break;
    }

    for (i = 0; i < linkArray.length; i = i + 1) {
        linkArray[i].sort(sortFunction);
    }

    window.top.epiMenuSortOrder = order;

    if (!!groups && groups.length > 0) {
        for (i = 0; i < groups.length; i = i + 1) {
            links = getElementsByClassName(groups[i], 'pageTypeLink');
            for (j = 0; j < links.length; j = j + 1) {
                var link = linkArray[i][j];
                links[j].href = link.href;
                SetInnerText(links[j], link.innerText);
                links[j].id = link.id;
            }
        }
    } else {
        for (i = 0; i < links.length; i = i + 1) {
            var link = linkArray[0][i];
            links[i].href = link.href;
            SetInnerText(links[i], link.innerText);
            links[i].id = link.id;
        }
    }
}



Steg 4, lägg till ikon.

Kopiera in en ikon i mappen Images/AdminMenu (under App_Themes), i mitt fall är sökvägen: "C:\Program Files\EPiServer\CMS\5.2.375.236\Application\App_Themes\Default\Images\AdminMenu".

Jag valde följande ikon:



(som jag kallade för PrefixGroup.jpg).

Steg 5, lägg till ikon-knapp i markup.

Återvänd till filen menu.aspx, och leta upp följande rad:


<img id="PageTypeAlphabeticalSorting" class="EPEdit-CommandTool" style="vertical-align: bottom; cursor: pointer;" src="<%=GetImageThemeUrl("AdminMenu/AlphabeticalSort.gif")%>" alt="<%=EPiServer.Core.LanguageManager.Instance.Translate("/admin/menuheadings/pagetypesorting/sortalphabetically")%>" title="<%=EPiServer.Core.LanguageManager.Instance.Translate("/admin/menuheadings/pagetypesorting/sortalphabetically")%>" onclick="changeSortOrder('alphabetical')" />

Lägg till följande efter denna rad:



<img id="PageTypeTreeGrouping" class="EPEdit-CommandTool" style="vertical-align: bottom; cursor: pointer;" src="<%=GetImageThemeUrl("AdminMenu/PrefixGroup.jpg")%>" alt="<%=EPiServer.Core.LanguageManager.Instance.Translate("/admin/menuheadings/pagetypegrouping/prefixgroup")%>" title="<%=EPiServer.Core.LanguageManager.Instance.Translate("/admin/menuheadings/pagetypegrouping/prefixgroup")%>" onclick="changeGrouping('prefix')" />


Steg 6, lägg till i språkfiler:

Välj önskade språkfiler i mappen för edit/admin-språkfiler, i ditt projekt. Dvs. webbapplikationens språk-mapp (som standard: /lang/language<språkändelse>.xml)

Leta upp elementet <pagetypesorting>...</...> och lägg till följande direkt efteråt (ex. engelska):

<pagetypegrouping>
  <prefixgroup>Group by prefix</prefixgroup>
</pagetypegrouping>



Sen är det klart!
Obs. på att EPiServer-uppdateringar som installeras antagligen förstör implementationen ovan.

Några skärmdumpar:

Utseende utan gruppering aktiverad.




















Grupperad och sorterad alfabetiskt.