Thursday 8 December 2011

Page Title in Joomla catalog extension SobiPro

SobiPro is an excellent catalog extension for Joomla - but while working with it I found one missing feature - ability to handle the page title separately.
For categories the Name field is used for page title, breadcrumb, category list and URL. For entries the Entry Title Field selected in the section konfiguration is used for page title, breadcrumb, entry lists and URL.
While this solution seems quite simple it is not SEO friendly. If the page title needs to be changed then the rest is also changed - including the URL which is now different.

I have not found any solution for this - so after spending a few hours I solved it in the backend code.

For category I use the introtext field for the pagetitle. For entry I created a custom field named field_pagetitle.

In the file /joomlaroot/components/com_sobipro/lib/base edit the file config.php and change the method getParentPath to:


public function getParentPath( $id, $names = false, $parents = false, $join = false, $useDescriptions = false )
{
/** @todo We have to implement the MySQL procedure for it */
$db =& SPFactory::db();
if( !( is_numeric( $id ) ) ) {
return false;
}
$ident = 'relations_path' . ( $names ? '_names' : '' ) . ( $parents ? '_parents' : '' ) . ( $join ? '_join' : '' );
$cached = SPFactory::cache()->getVar( $ident, $id );
if( $cached ) {
return $cached;
}
else {
$cid = $id;
}
$path = $parents ? array() : array( $id );
while ( $id > 0 ) {
try {
$db->select( 'pid', 'spdb_relations', array( 'id' => $id ) );
$id = $db->loadResult();
if( $id ) {
$path[] = ( int ) $id;
}
}
catch ( SPException $x ) {
Sobi::Error( __FUNCTION__, SPLang::e( 'CANNOT_GET_PARENT_ID', $x->getMessage() ), SPC::WARNING, 500, __LINE__, __CLASS__ );
}
}
$introtexts;
if( $names && count( $path ) ) {
if ($useDescriptions) {
$names = SPLang::translateObject( $path, 'name', array( 'category' ) );
$introtexts = SPLang::translateObject( $path, 'introtext', array( 'category' ) );
}
else {
$names = SPLang::translateObject( $path, 'name', array( 'section', 'category', 'entry' ) );
}
if( is_array( $names ) && !empty( $names ) ) {
foreach ( $path as $i => $id ) {
if( $join ) {
$path[ $i ] = array( 'id' => $id, 'name' => $names[ $id ][ 'value' ], 'description' => $introtexts[ $id ][ 'value' ] );
}
else {
$path[ $i ] = $names[ $id ][ 'value' ];
}
}
}
}
$path = array_reverse( $path );
SPFactory::cache()->addVar( $path, $ident, $cid );
return $path;
}


In the file /joomlaroot/components/com_sobipro/lib/base edit the file mainframe.php and change the method addObjectToPath to:


public function addObjToPathway( $obj ) {
if( defined( 'SOBI_ADM_PATH' ) ) {
return true;
}
$menu =& JSite::getMenu()->getActive()->query;
$sid = isset( $menu[ 'sid' ] ) ? $menu[ 'sid' ] : 0;
$pathway = & JFactory::getApplication()->getPathway();
if( $obj->get( 'oType' ) == 'entry' ) {
$id = SPRequest::int( 'pid' );
/* if we didn't enetered this entry via category */
if( !$id || $id == Sobi::Section() || Sobi::Cfg( 'entry.primary_path_always' ) ) {
$id = $obj->get( 'parent' );
}
}
else {
$id = $obj->get( 'id' );
}
$path = SPFactory::cache()->getVar( 'parent_path', $id );
if( !( $path ) ) {
$path = SPFactory::config()->getParentPath( $id, true, false, true, true );
SPFactory::cache()->addVar( $path, 'parent_path', $id );
}

if( count( $path ) ) {
/* skip everything above the linked sid */
$rpath = array_reverse( $path );
$path = array();
foreach ( $rpath as $part ) {
if( $part[ 'id' ] == $sid ) {
break;
}
$path[] = $part;
}
$path = array_reverse( $path );
/* ^^ skip everything above the linked sid */
}
$title = array();


if( count( $path ) ) {
foreach ( $path as $data ) {
$title[] = ($data[ 'description' ] != '' ? $data[ 'description' ] : $data[ 'name' ]);
$pathway->addItem( $data[ 'name' ], htmlentities( self::url( array( 'title' => $data[ 'name' ], 'sid' => $data[ 'id' ] ) ) ) );
}
}
if( $obj->get( 'oType' ) == 'entry' ) {
$pathway->addItem( $obj->get( 'name' ), htmlentities( self::url( array( 'task' => 'entry.details','title' => $obj->get( 'name' ), 'sid' => $obj->get( 'id' ) ) ) ) );
$title[] = $obj->get( 'field_pagetitle' );
}
$this->setTitle( $title );
}


Remember to add the field field_pagetitle to the entry and select a different field to the Entry Title Field. Depending on your template you might need to change some things in there too.

Friday 29 July 2011

'Sys' is undefined error on SharePoint 2007 (and ASP.NET)

When using the ScriptHandler module you may receive the 'Sys' is not defined JavaScript error. This is due to missing configurations in web.config in your SharePoint virtual folder.

Put the below settings into web.config and do an iisreset (note you may have to change the version and PublicKeyTokens to the assemblies, you have installed)

<system.web>

<pages>

<controls>

<add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

</controls>

</pages>



<compilation debug="false">

<assemblies>

<add assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

</assemblies>

</compilation>


<httpHandlers>

<remove verb="*" path="*.asmx"/>

<add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/>

</httpHandlers>

<httpModules>

<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

</httpModules>

</system.web>

Wednesday 20 April 2011

How to edit nested ListView in ASP.NET

ListView is a cool control in ASP.NET. It is usefull for - well - listing data.

Even cooler you can nest a ListView inside another ListView if you need to group your list into groups - e.g. log grouped by date or an OrderHeader -&amp;gt; OrderLines grouping - and it is easy.

Unfortunately editing nested items is a bit clumcy - and it took me some time to figure it out. I have made an example here with two nested ListViews.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="NesterListTest.WebForm1" EnableEventValidation="false" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="
http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

        <asp:ListView ID="ListView1" runat="server" DataKeyNames="Id" OnItemDataBound="BindNestedList">
        <LayoutTemplate>
            <table id="RegistrationLines">
                <tr id="itemPlaceholder" runat="server"></tr>
            </table>
        </LayoutTemplate>
        <ItemTemplate>
            <tr><th>OuterIndex: <%# Eval("Id") %></th></tr>
           
            <asp:ListView ID="ListView2" runat="server" DataKeyNames="Id" OnItemEditing="EditNestedItem">
            <ItemTemplate>
                <tr>
                    <td><%# Eval("Value") %></td>
                    <td><asp:Button runat="server" ID="EditButton" CommandName="edit" Text="Edit" /></td>
                </tr>
            </ItemTemplate>
            <EditItemTemplate>
                <tr>
                    <td>Now editing -></td>
                    <td><%# Eval("Value") %></td>
                </tr>
            </EditItemTemplate>
            </asp:ListView>
        </ItemTemplate>
        </asp:ListView>  
    </div>
    </form>
</body>
</html>


In the Page_Load I just create some funke sample data. Note that I have two classes. These are just for holding the data - nothing else.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace NesterListTest
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        List<TestContainer> list;

        protected void Page_Load(object sender, EventArgs e)
        {
            list = new List<TestContainer>();

            for (int i = 0; i < 5; i++)
            {
                TestContainer tc = new TestContainer() { Id = i };

                List<TestItem> l = new List<TestItem>();
                for (int j = 0; j < i % 3; j++)
                {
                    TestItem t = new TestItem() { Id = j, Value = "Cheese " + i + " > " + j };
                    l.Add(t);
                }
                tc.Items = l.ToArray();
                list.Add(tc);
            }

            ListView1.DataSource = list;
            ListView1.DataBind();
        }

        protected void BindNestedList(Object sender, ListViewItemEventArgs e)
        {
            if (e.Item.ItemType == ListViewItemType.DataItem)
            {
                ListViewDataItem item = (ListViewDataItem)e.Item;
                TestContainer container = (TestContainer)item.DataItem;
                ListView innerList = (ListView)item.FindControl("ListView2");
                innerList.DataSource = container.Items;
                innerList.DataBind();
            }
        }

        protected void EditNestedItem(Object sender, ListViewEditEventArgs e)
        {
            ListView lvs = sender as ListView;

            ListViewDataItem ctl = (ListViewDataItem)ListView1.FindControl(lvs.Parent.ID);
            if (ctl != null)
            {
                ListView lv = (ListView)ctl.FindControl("ListView2");
                lv.EditIndex = e.NewEditIndex;

                int indexOfParentControl = ListView1.Items.IndexOf(ctl);
                TestContainer t = list[indexOfParentControl];
                lv.DataSource = t.Items;
                lv.DataBind();
            }
        }
    }

    public class TestContainer
    {
        public int Id { get; set; }
        public TestItem[] Items { get; set; }
    }

    public class TestItem
    {
        public int Id { get; set; }
        public string Value { get; set; }
    }
}



In the EditNestedItem method, I find the parent control of the sender. As the sender is the inner ListView, the parent of this is a ListViewDataItem of the outer ListView. In the ListViewDataItem I find the inner ListView and set the EditIndex of it.
I then find the index of the ListViewDataItem in the outer ListViews items collection and now I have the index of the TestContainer for ListViewDataItem. Then I can grap it from the list and bind the Items of the TestContainer to the inner ListView.