# Friday, May 29, 2009

Getting errors nicely via email from your website the vb.net version

For the c# version look here -> http://www.myfriedmind.com/techBlog/2009/05/29/GettingErrorsNicelyViaEmailFromYourWebsiteTheCVersion.aspx

It is always nice to get notified that there is an error on your website in some other way than a user on the other end of the phone.

One of the nicest ways I know of in .net is to use the Application_Error portion of Global.asax to shoot me an email listing the information that is needed. I have included some basic code below but you can feel free to modify it as you choose. For example you may prefer to include the System.Net.Mail info on the SmtpServer in the web.config so you can use it on other portions of the site rather than repeat it.

In the code I use reflection to roll through both the Request object and the Exception that is passed. You can modify it if you want to display more (such as innerexceptions, etc) or to exclude, but Reflection can be nice in pulling up all the data quickly.

=================
In vb.net

In Web.Config
        <customErrors mode="On" defaultRedirect="~/ErrorOnWebsite.aspx"/>

In Global.asax (don't do this and you get an endless loop!)
    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        Server.Transfer("~/ErrorOnWebsite.aspx")
    End Sub

On ErrorOnWebsite.aspx (you don't actually have to use a label, you can provide some other means of apology)
        <asp:Label runat="server" ID="lblApology"></asp:Label>

This is Error.OnWebsite.aspx.vb (I like code-behinds) - note the use of the HtmlTextWriter and Reflection!

Imports System
Imports System.IO
Imports System.Net.Mail
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Reflection

Partial Class ErrorOnPage
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Dim exc As Exception = Server.GetLastError()

        If (exc Is Nothing) Then
            ' there is no error, why are they here?
            Response.Redirect("~/")
        Else
            SendNotificationEmail(exc)
            WriteApology()
            Server.ClearError()
        End If
    End Sub

    Private Sub WriteApology()
        lblApology.Text = "Oops, sorry, the master of light and sound has been notified..."
    End Sub

    Private Sub SendNotificationEmail(ByVal exc As Exception)
        ' Create the format holder for the data, I like tables...
        Dim _infoTable As Table = New Table()
        _infoTable.Attributes.Add("border", "1")
        _infoTable.Attributes.Add("cellpadding", "3")

        Dim _infoRow As TableRow
        Dim _infoNameCell As TableCell
        Dim _infoDataCell As TableCell

        _infoRow = New TableRow()
        _infoNameCell = New TableCell()
        _infoNameCell.Text = "Server Request Properties"
        _infoNameCell.BackColor = System.Drawing.Color.Gray
        _infoNameCell.ColumnSpan = 2
        _infoRow.Cells.Add(_infoNameCell)
        _infoTable.Rows.Add(_infoRow)

        ' I like to use reflection because I hate to type...
        Dim _requestType As Type = Request.GetType()
        Dim _pInfo As PropertyInfo

        For Each _pInfo In _requestType.GetProperties()

            If (Not _pInfo.Name = "Item") Then

                _infoRow = New TableRow()

                _infoNameCell = New TableCell()
                _infoNameCell.Text = _pInfo.Name
                _infoRow.Cells.Add(_infoNameCell)

                _infoDataCell = New TableCell()
                Dim _value As Object = _pInfo.GetValue(Request, Nothing)
                If _value Is Nothing Then
                    _infoDataCell.Text = ""
                Else
                    _infoDataCell.Text = Server.HtmlEncode(_value.ToString())
                End If
                _infoRow.Cells.Add(_infoDataCell)

                _infoTable.Rows.Add(_infoRow)
            End If
        Next

        _infoRow = New TableRow()
        _infoNameCell = New TableCell()
        _infoNameCell.Text = "Exception Properties"
        _infoNameCell.BackColor = System.Drawing.Color.Gray
        _infoNameCell.ColumnSpan = 2
        _infoRow.Cells.Add(_infoNameCell)
        _infoTable.Rows.Add(_infoRow)

        Dim _exceptionType As Type = exc.GetType()
        For Each _pInfo In _exceptionType.GetProperties()        
            _infoRow = New TableRow()

            _infoNameCell = New TableCell()
            _infoNameCell.Text = _pInfo.Name
            _infoRow.Cells.Add(_infoNameCell)

            _infoDataCell = New TableCell()
            Dim _value As Object = _pInfo.GetValue(exc, Nothing)
            If (_value Is Nothing) Then
                _infoDataCell.Text = ""
            Else
                _infoDataCell.Text = Server.HtmlEncode(_value.ToString())
            End If
            _infoRow.Cells.Add(_infoDataCell)

            _infoTable.Rows.Add(_infoRow)
        Next

        ' note the use of the HtmlTextWriter to render the table
        ' into a string
        Dim _bodyWriter As StringWriter = New StringWriter()
        Dim _writeToBody As HtmlTextWriter = New HtmlTextWriter(_bodyWriter)
        _infoTable.RenderControl(_writeToBody)

        Dim _message As MailMessage = New MailMessage("error@mysite.com", "myemail@mysite.com")
        _message.Subject = String.Format("Error on website - {0}", exc.Message)
        _message.Body = _bodyWriter.ToString()
        _message.IsBodyHtml = True

        Dim _mailClient As SmtpClient = New SmtpClient("mymailserver")
        _mailClient.Send(_message)

    End Sub

End Class

Getting errors nicely via email from your website the c# version

For the vb.net version - go here -> http://www.myfriedmind.com/techBlog/2009/05/29/GettingErrorsNicelyViaEmailFromYourWebsiteTheVbnetVersion.aspx

It is always nice to get notified that there is an error on your website in some other way than a user on the other end of the phone.

One of the nicest ways I know of in .net is to use the Application_Error portion of Global.asax to shoot me an email listing the information that is needed. I have included some basic code below but you can feel free to modify it as you choose. For example you may prefer to include the System.Net.Mail info on the SmtpServer in the web.config so you can use it on other portions of the site rather than repeat it.

In the code I use reflection to roll through both the Request object and the Exception that is passed. You can modify it if you want to display more (such as innerexceptions, etc) or to exclude, but Reflection can be nice in pulling up all the data quickly.

=================
In c#

In Web.Config
        <customErrors mode="On" defaultRedirect="~/ErrorOnWebsite.aspx"/>

In Global.asax (don't do this and you get an endless loop!)
    void Application_Error(object sender, EventArgs e)
    {
        Server.Transfer("~/ErrorOnWebsite.aspx");
    }

On ErrorOnWebsite.aspx (you don't actually have to use a label, you can provide some other means of apology)
        <asp:Label runat="server" ID="lblApology"></asp:Label>

This is Error.OnWebsite.aspx.cs (I like code-behinds) - note the use of the HtmlTextWriter and Reflection!

using System;
using System.IO;
using System.Net.Mail;
using System.Reflection;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class ErrorOnWebsite : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Exception exc = Server.GetLastError();

        if (exc != null)
        {
            SendNotificationEmail(exc);
            WriteApology();
            Server.ClearError();
        }
        else
        {
            // if there is no error, why are they here?
            Response.Redirect("~/", true);
        }
    }

    private void WriteApology()
    {
        lblApology.Text = "Oops, sorry, the master of light and sound has been notified...";
    }

    private void SendNotificationEmail(Exception exc)
    {
        // Create the format holder for the data, I like tables...
        Table _infoTable = new Table();
        _infoTable.Attributes.Add("border", "1");
        _infoTable.Attributes.Add("cellpadding", "3");

        TableRow _infoRow;
        TableCell _infoNameCell;
        TableCell _infoDataCell;

        _infoRow = new TableRow();
        _infoNameCell = new TableCell();
        _infoNameCell.Text = "Server Request Properties";
        _infoNameCell.BackColor = System.Drawing.Color.Gray;
        _infoNameCell.ColumnSpan = 2;
        _infoRow.Cells.Add(_infoNameCell);
        _infoTable.Rows.Add(_infoRow);

        // I like to use reflection because I hate to type...
        Type _requestType = Request.GetType();
        foreach (PropertyInfo _pInfo in _requestType.GetProperties())
        {
            if (_pInfo.Name != "Item")
            {
                _infoRow = new TableRow();

                _infoNameCell = new TableCell();
                _infoNameCell.Text = _pInfo.Name;
                _infoRow.Cells.Add(_infoNameCell);

                _infoDataCell = new TableCell();
                object _value = _pInfo.GetValue(Request, null);
                if (_value != null)
                {
                    _infoDataCell.Text = Server.HtmlEncode(_value.ToString());
                }
                else
                {
                    _infoDataCell.Text = "";
                }
                _infoRow.Cells.Add(_infoDataCell);

                _infoTable.Rows.Add(_infoRow);
            }
        }

        _infoRow = new TableRow();
        _infoNameCell = new TableCell();
        _infoNameCell.Text = "Exception Properties";
        _infoNameCell.BackColor = System.Drawing.Color.Gray;
        _infoNameCell.ColumnSpan = 2;
        _infoRow.Cells.Add(_infoNameCell);
        _infoTable.Rows.Add(_infoRow);

        Type _exceptionType = exc.GetType();
        foreach (PropertyInfo _pInfo in _exceptionType.GetProperties())
        {
            _infoRow = new TableRow();

            _infoNameCell = new TableCell();
            _infoNameCell.Text = _pInfo.Name;
            _infoRow.Cells.Add(_infoNameCell);

            _infoDataCell = new TableCell();
            object _value = _pInfo.GetValue(exc, null);
            if (_value != null)
            {
                _infoDataCell.Text = Server.HtmlEncode(_value.ToString());
            }
            else
            {
                _infoDataCell.Text = "";
            }
            _infoRow.Cells.Add(_infoDataCell);

            _infoTable.Rows.Add(_infoRow);
        }

        // note the use of the HtmlTextWriter to render the table
        // into a string
        StringWriter _bodyWriter = new StringWriter();
        HtmlTextWriter _writeToBody = new HtmlTextWriter(_bodyWriter);
        _infoTable.RenderControl(_writeToBody);

        MailMessage _message = new MailMessage("error@mysite.com", "myemail@mysite.com");
        _message.Subject = String.Format("Error on website - {0}", exc.Message);
        _message.Body = _bodyWriter.ToString();
        _message.IsBodyHtml = true;

        lblApology.Text = _bodyWriter.ToString();

        SmtpClient _mailClient = new SmtpClient("mymailserver");
        _mailClient.Send(_message);
    }

Howto: load a text file quickly into a DataTable


Someone asked about a quick way to load a text file and since the code is a bit lengthy I have included it before.

Basically one of the quickest ways (rather than opening and reading it line by line by line by line) is to use the OleDb connection to quickly push it into a Dataset. You can do this with any delimiter...

There are two procedures below - the first one UpdateSchemaIni does what it says - it updates the Schema.Ini so that the OleDbConnection knows what to parse on. This is necessary for non-csv files. The second - LoadDataFile - loads up the file and sticks it into a table of your naming and then returns a DataSet for you to play with.

Enjoy!!!

============ code below

private char m_delimiter

/// <summary>
/// Required to write the Schema.Ini in case the delimiter for the files are not simply CSV
/// </summary>
/// <param name="m_delimiter">System.Char representing the delimter to use in this file</param>
/// <param name="filename">Filename to pass, helping to locate the Schema.Ini and give the header for the section</param>
private static void UpdateSchemaIni(char m_delimiter, string filename)
{
    string _schemaFile = String.Format(@"{0}\{1}", Path.GetDirectoryName(filename), "Schema.ini");
    string _filenameHeader = String.Format("[{0}]", Path.GetFileName(filename)).ToLower();
    string _delimiterLine = String.Format("Format=Delimited({0})", m_delimiter);

    StreamReader _reader;
    StreamWriter _writer;

    string _tempFile = String.Format("{0}.{1}", _schemaFile, DateTime.Now.Ticks);
    while (File.Exists(_tempFile))
    {
        _tempFile += "0";
    }

    if (!File.Exists(_schemaFile))
    {
        using (_writer = new StreamWriter(_schemaFile))
        {
            _writer.WriteLine(_filenameHeader);
            _writer.WriteLine(_delimiterLine);
            _writer.Close();
        }
    }
    else
    {
        bool _successfullyUpdated = false;
        using (_writer = new StreamWriter(_tempFile))
        {
            using (_reader = new StreamReader(_schemaFile))
            {
                bool _entryOnNextLine = false;
                while (_reader.Peek() > 0)
                {
                    string _currentLine = _reader.ReadLine();
                    if (_currentLine.Trim().ToLower() == _filenameHeader)
                    {
                        _writer.WriteLine(_currentLine);
                        _entryOnNextLine = true;
                    }
                    else if (_entryOnNextLine)
                    {
                        _writer.WriteLine(_delimiterLine);
                        _entryOnNextLine = false;
                        _successfullyUpdated = true;
                    }
                    else
                    {
                        _writer.WriteLine(_currentLine);
                    }
                }
                _reader.Close();
            }
            if (!_successfullyUpdated)
            {
                _writer.WriteLine(_filenameHeader);
                _writer.WriteLine(_delimiterLine);
                _successfullyUpdated = true;
            }
            _writer.Close();
        }
        if (_successfullyUpdated)
        {
            File.Copy(_tempFile, _schemaFile, true);
            File.Delete(_tempFile);
        }
        else
        {
            throw new ApplicationException("Unable to updated Schema.Ini file");
        }
    }
}

private static DataTable LoadDataFile(string filename, string tableName)
{
    if (File.Exists(filename))
    {
        UpdateSchemaIni(m_delimiter, filename);
        FileInfo _fileInfo = new FileInfo(filename);

        string _connectionString =
             String.Format(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties='Text;HDR=YES;FMT=Delimited({1})';",
            _fileInfo.DirectoryName, m_delimiter);
        OleDbConnection _connection;
        using (_connection = new OleDbConnection(_connectionString))
        {
            DataTable _dataTable = new DataTable(tableName);
            _connection.Open();
            OleDbCommand _selectCommand = new OleDbCommand(String.Format("SELECT * FROM [{0}]", _fileInfo.Name), _connection);
            OleDbDataAdapter _adapter = new OleDbDataAdapter();
            _adapter.SelectCommand = _selectCommand;
            try
            {
                _adapter.Fill(_dataTable);
            }
            catch (Exception exc)
            {
                throw new ApplicationException(String.Format("Error when trying to create a table from file {0}. {1}", filename, exc.Message));
            }
            return _memberTable;
        }
    }
    else
    {
        throw new ApplicationException(String.Format("Specified file does not exist, unable to load file - {0}", filename));
    }
}

The usefulness of the DateTime Date property

I have run across a number of questions in the asp.net forum that are easily solved with this little-known but very useful property of DateTime objects - namely the Date property. I figured it would be worth a quick post because where it is handy it is very handy...

All Date does is simply zero out the hours, leaving you with the Date set to 12 am (or 00:00:00). This makes it simple, for example to see if two different DateTime objects occur on the say day, regardless of the hour without having to do any fancy multi-property comparisons:

myFirstDateTime.Date.CompareTo(mySecondDateTime.Date)

You could also do the same with using the ToShortDateTimeString and using a string comparison, but besides the conversion (and the issues re strings objects in general) why not use something native to DateTime?

# Thursday, May 28, 2009

Unable to add selected web part(s). Cannot import this Web Part.

Recently ran across this trying to install a customized webpart. Starting going over everything with a fine tooth comb. Even mocked up a .wepbart xml to get it to run.
 
Finally ran it in VS2005 on a 'test' site - turns out I had forgotten to add PUBLIC to the class so that it could be accessed.
 
ooops. won't do that again. actually I probably will...
 
;-)
 
Once again, another verbose Sharepoint Message...

# Wednesday, May 27, 2009

Encrypting your Web.Config on an FBA Site

I was being a good boy *SHOCK* and encrypting my web.config sections on my MOSS server like I was supposed to. Except...
 
Except...
 
It worked like a charm on the Windows authentication side, but the FBA side was all wacky. Throwing:

Failed to decrypt using provider 'MossCustomKey'. Error message from the provider: The RSA key container could not be opened.

So what to do? After a bunch o hunting I kept coming across this issue - it is permissions - need to allow the account to have permissions.
 
This is easly accomplished using the aspnet_regiis -pa "MyCustomKey" "AccountToGiveAccess" command line. But what account?
 
I tried "Nt Service". I tried "ASPNET". I tried the Application Pool account. But here is the clincher, because this allows Anonymous access, the account is whatever you have the Anonymous Account set to in IIS - EVEN AFTER YOU HAVE LOGGED ON USING FBA!!!!
 
This comes from the <identity impersonate='true' /> line in the web.config and how Sharepoint uses it. Note that you COULD set a specific impersonate there, but I am not sure you really want to (although I may experiment more in my quest to enable Sharepoint to assign anon permissions at a page, not subsite, level).
 
So what was the account - simply the %machinename%\IUSR_%machinename% account...
 
Remember to do this on ALL your servers on the farm... (as well as exporting the XML!)
 
***** FURTHER INFO *****
 
I appear to be running into a problem with my Intranet access and encryption. Occasionally it will throw the error above almost as if it has lost its ability to authorize people. Have to iisreset, and sometimes reboot, in order to resolve. Perhaps trouble with enumerating Domain Users group membership?
# Tuesday, May 26, 2009

Sharepoint 2007 Properties Snippet

Here is a snippet I created that I like to use when defining properties for webparts. Have fun with it...

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
   <CodeSnippet Format="1.0.0">
      <Header>
         <Title>Sharepoint WebPart Property</Title>
         <Shortcut>wpprop</Shortcut>
         <Description>Code snippet for Sharepoint Webpart Property.</Description>
         <Author>Matthew Mcknight</Author>
         <SnippetTypes>
            <SnippetType>Expansion</SnippetType>
         </SnippetTypes>
      </Header>
      <Snippet>
         <Declarations>
            <Literal>
               <ID>type</ID>
               <ToolTip>Property type</ToolTip>
               <Default>string</Default>
            </Literal>
            <Literal>
               <ID>property</ID>
               <ToolTip>Property name</ToolTip>
               <Default>MyProperty</Default>
            </Literal>
            <Literal>
               <ID>field</ID>
               <ToolTip>The variable backing this property</ToolTip>
               <Default>_myProperty</Default>
            </Literal>
            <Literal>
               <ID>personalizable</ID>
               <Type>System.Web.UI.WebControls.WebParts.PersonalizationScope</Type>
               <ToolTip>Personalization Level for this webpart?</ToolTip>
               <Default>PersonalizationScope.Shared</Default>
            </Literal>           
            <Literal>
               <ID>browsable</ID>
               <Type>System.Boolean</Type>
               <ToolTip>Is this webpart browsable?</ToolTip>
               <Default>true</Default>
            </Literal>           
     <Literal>
               <ID>description</ID>
               <ToolTip>The description for this webpart.</ToolTip>
               <Default>My Description</Default>
            </Literal>
            <Literal>
               <ID>defaultValue</ID>
               <ToolTip>The default value for this property.</ToolTip>
               <Default></Default>
            </Literal>
     <Literal>
               <ID>category</ID>
               <ToolTip>The category to display for this webpart in the webpart editor.</ToolTip>
               <Default>My Category</Default>
            </Literal>
     <Literal>
               <ID>friendlyName</ID>
               <ToolTip>The friendly name to display for this webpart.</ToolTip>
               <Default>My Friendly Name</Default>
            </Literal>
         </Declarations>
         <Code Language="csharp"><![CDATA[
private $type$ $property$;

[Personalizable($personalizable$)]
[WebBrowsable($browsable$)]
[Description("$description$")]
[DefaultValue("$defaultValue$")]
[Category("$category$")]
[FriendlyName("$friendlyName$")]
public $type$ $property$
{
 get { return $field$; }
 set { $field$ = value; }
}
$end$]]>
         </Code>
      </Snippet>
   </CodeSnippet>
</CodeSnippets>

# Friday, May 22, 2009

Curse you PublishingPageLayout!!!!!! Or the discovery of a wonderful "feature" of Sharepoint 2007

*** Please Note that this is done on a "publishing" site, it may be that this issue (I mean 'feature') does not occur on other types of sites. But I doubt it.
 
For those of you unfamiliar with Sharepoint, it combines Master Pages (for overarching look and feel or every page in the site) with Page Templates (which help define how a specific page should be layed out but not the content itself). One way to think of it is that each page in Sharepoint has three levels:

  1. The master page is the highest level and applies to all pages (unless overriden by a page or subsite)
  2. The page template layout determines what 'type' of page (ie web parts page, or article page, or welcome page, or ...)
  3. The actual page content.

Remeber that Master Pages can have parent Master Pages. Page Template Layouts are not considered Master Pages by Sharepoint, but for those of you familiar with the concept of parent/child Master Pages, Page Template Layouts act in the manner similar to a Child Master Page. 

Using Page Template Layouts works wonderfully on Sharepoint unless you have ...

  • more than one server, or ...
  • at least one extension to the server, or ...
  • you move to a new server, or ...
  • you move files from one server to another (via publishing, Sharepoint Designer, manual, etc).

In other words, unless you have a single url to use which never, ever changes, you may be in for trouble.
 
Why? Well it's all relative. Or more specifically it is NOT relative, it is absolute.
 
Basically whenever you create a page it prepends the page layout template section with the URL that you created the page on, turning it into an absolute reference. In other words if you take a look at your page (ex by downloading a copy) you will find that there is a section that reads like this:
 
<mso:CustomDocumentProperties>
... stuff here
<mso:PublishingPageLayout msdt:dt="string">http://theurlthatyouused/_catalogs/masterpage/PageFromDocLayout, Article page with body only</mso:PublishingPageLayout>
... more stuff here
</mso:CustomDocumentProperties>
 
Note that it has prepended the url, whether it be interal - http://myserver:8080 - or external one - http://www.myfriedmind.com.
 
So what does this mean for you? I have good news and bad news.

  • The good news: even if the URL of the page is different (eg calling the page from http://myserver:9090 and not http://myserver:8080 if you extended your site) the page will still show up. Even though the absolute url is different it still pulls it from the database as if it was a relative url. Strange but true.
  • The bad news: You will not be able to edit the page settings from other than the original URL.

If you do, expect this to appear:


 
WHA????????????? Where did that come from? Did someone else modify it? Nope.
 
After choosing Exit Without Saving you might get it to work. Maybe.
 
You might also get this error if you have a webpartpage and decide to edit one of the webparts. Keep clicking on choices until one of them works...
 
Does this remind anyone of being lost in the caverns of Adventures of Zelda? You are in a maze of twisty little passages, all alike...
 
The best is if you try to modify the Page Settings on such a page...

*BOOM*

What has happened is that Sharepoint is looking at the PageTemplateLayout property of the page (which is an absolute reference). If the Server Name and/or Port that you are editing from do not match that exactly then Sharepoint assumes that someone has modified the page between when you started editing and when you try to save it.

Again, one of the ways that Sharepoint tries to determine if your copy is 'dirty' (ie someone has made a change between when you first tried to edit and now) is that it looks at the PageTemplateLayout property. If there is not an exact match it rejects it. If you try to modify the Page Settings it explodes.

  • Scenario #1 - you copy a page from a different URL using Sharepoint Designer - it simply copies the file, including the now INCORRECT URL.
  • Scenario #2 - You are moving your sharepoint to a bigger, faster server (or you are restoring). The new server has a different name...
  • Scenario #3 - You are publishing the pages out to a different front end server but want to be able to edit it from there...
  • Scenario #4 - you are extending a web app to be used on the internet, and you want both internal and external people to be able to edit the same pages, but using different URLs...

etc, etc, etc...
 
What is the solution? Well, there are a couple. One is to use this nifty addition to STSADM - http://stsadm.blogspot.com/2007/08/fix-publishing-pages-page-layout-url.html. Gentleman Gary has done a wonderful job creating a flexible way to repair these beasties. The only caveat - it is an incomplete fix. Basically it still uses an absolute URL, it just replaces the wrong one with the correct one (see his note at the bottom in response to my question). It will fix #1 and #2 above (although if you move it again you will have to rerun it again), but it does nothing for #4. I am not sure about #3 <g>.

Again, note that this does not fix the issue with multiple access, only where a page has been moved. However, if it is simply a matter that you have moved your pages to a different server than this is a pretty rocking solution.
 
I have to deal with #4 after doing #2, so what do I do?

Initially I personally download each file as I uncovered the issue and manually removed the absolute url from the text and replaced it with a relative url and then uploaded the file. I expected a handful to show up, but the number kept increasing. In addition it was a pain for our staff. Sometimes I could spot it after they had made a non-blowing up modification by checking the nvarchar14 in the content database (Select tp_id, nvarchar14 from AllUserData Where nvarchar14 LIKE 'http://%') , but sometimes I would only catch if when they called me to complain.

Btw, if you are thinking that you can just modify the nvarchar14 instead of extracting and modifying the page, it does not work. It will be overwritten once another change is made to put it back to the absolute url. Trust me.

My solution ultimately was to write some code in c# that:

  1. extracted each page
  2. checked it out
  3. inspected the PageTemplateLayout text for an absolute URL
  4. replaced it with a relative URL if that was the case
  5. uploaded the page
  6. checked in the page
  7. published the page
  8. approved the page
I may publish that code in another blog on the off chance anyone is curious about how to do such a thing. This solves Scenario #2 and #4 for me.

Long term? Microsoft???? 

Maybe this is part of the design, or maybe they just missed this. I am just hoping they will fix it. There appears to be absolutely no loss of functionality with a relative reference and even thought there *might* be a case where a page template is housed on another site (and hence a different URL) this is far too serious an issue to sweep under the carpet.
 
I will enter more on another PageTemplate issue in another blog...

# Wednesday, May 20, 2009

Ajax history and the Memento pattern

Note: special thanks to Thomas who catches my error in using QueryString vs url hash - I have made the appropriate changes in this entry...

Part 1 - Introduction
Part 2 - Basic Example
Part 3 - Complex Example
Part 4 - Final Notes
Bonus - Ajax History and the Memento Pattern
Extra Bonus - Issues with Opera

Recently while watching a geekspeak on Ajax history it struck me that it reminded me strongly of a Memento pattern. I mentioned this to Glen Gordon and he recommended I write a blog on it, so without further ado...

First off, a little background. Maintaining history has been one of the holy grails of Ajax since it began. It is a wonderful feeling to produce a zippy, intiutive site that has fantastic UI, but once someone hits the "back" button everything goes down the tube. The reason is simple, browsers were created in the time before, even before Javascript, much less XML. Back then the "back" button, or any sort of history action, brought you back not to your last action but to the last page that fully loaded.

To give a parallel, for anyone who has used the "undo" command in whatever program, what it does is "undo" the last action that you did. Imagine typing for hours in Word, clicking "undo", and having it wipe out EVERYTHING that you had just done, not just the last little action. No one would want that, but this is what the "back" key effectively did by default. Hence, the quest to maintain Ajax history. Wouldn't it be nice if the "back" key, or other "back" command acted as an "undo" vs a true "back to the previous page"?

In addition, what if after working on a page (say a maps page) that uses Ajax, you want to send someone the link. The option is to use a URL, but since Ajax by default modified the page but not the URL, all that work would be useless. You would think you were sending them a map to your cabin by the lake, they would simply get the default map of the world. Not good.

There have been ways to resolve this even before .net 3.5 came out. One merely has to look at some of the larger Ajax enabled sites (such as google maps) to see this, but this is such a constant and complex requirement in almost any Ajax development that it is only logical that someone develop a abstractive pattern to simplify and standardize it. The 3.5 framework does this, one of many reason it makes me squeal like schoolgirl. But what is especially fun to look at is how it does it, specifically what pattern was chosen. The pattern, which is extremely logical to choose, is akin to the Memento pattern.

I won't go into Design Patterns here. If you are not sure, follow this link, buy the book, get the poster. I want to focus on one pattern, the Memento pattern and how it relates to Ajax history in the hopes that it will illumine your coding.

The Memento pattern requires two players, commonly refered to as the Originator and the Caretaker. To clarify the interaction:

  1. The Caretaker is going to change the Originator in some way but the Originator needs to be able to undo the change.
  2. The Caretaker receives a small "memento" object and stores it (hence the Caretaker name).
  3. The Caretaker then changes the Originator.

If the change needs to be undone:

  1. The Caretaker returns the "memento" object to the Originator.
  2. The Originator uses the "memento" object to undo the change.

But it is also critical to understand about the "memento".

  • The "memento" is an opaque object, meaning the Caretaker can not change it
  • The "memento" is small, it is not the entire Originator, merely the minimal information needed to rollback state
  • The Originator is the one who packages and unpackages the "memento"
  • The Originator is the one who rolls back state (ie, the Caretaker does not modify the Originator to roll it back)

So how does this apply to how .Net does Ajax history? In short, .Net uses the URL of the page as the "memento", the History as the Caretaker, with the code you write in the framework acting as the Originator.

  1. The Ajax action is going to change the webpage someway.
  2. The current hash is already stored
  3. The Ajax action makes the change and alters the hash

If the change needs to be undone:

  1. The hash (in the URL) that was from the previous page (the rollback state) is returned.
  2. The code that YOU wrote rollsback the state.

In this case the memento has these characteristics

  • The "memento" is a url which is unchanged by the Caretaker (browser)
  • The "memento" is small (you should make sure of this)
  • The framework writes the URL that you need and allows you to access it
  • Your code is what rolls back state

So, what does this mean? Basically the key point is that you can apply the practices that you have in place for working with the Memento pattern to working with Ajax history. Chief among these is that the "memento" should be small! Do not try to squeeze too much into your URL. It is not pretty and there are limits! Instead pare down to the minimum of what you need to be able to restore state.

One major bonus is that this not merely allows rollback, but enables your users to use URLs to return an Ajax-enabled page on which certain steps have already been taken. This is AMAZING!!!!!!! And compared to how we used to have to do it, it is like a walk in the park!

 

# Monday, May 18, 2009

Sharepoint Backup Permissions or SqlException: Cannot open backup device '\\myserver\backups\spbr0000\00000024.bak'. Device error or device off-line

For those of you who are using Sharepoint's 'built-in' backup feature, you probably already know that one of the biggest problems with it is that you can not automate it directly. In order to do that you need to used AT ->
AT 01:00 /every:1,2,3,4,5,6,7 cmd /c "stsadm.exe -o backup -directory file://myserver/mybackupshare -backupmethod full"
 
This is not a showstopper. It works. However unless you have the correct perms it will fail. Or, to be more specific, the Sql Service Account needs to be able to read/write to that share. Again, this is the account that the Sql Service runs under. Hopefully a domain account.
 
Also, if you are going to run it from the Sharepoint Central Administration Website, the account you are logged in with needs those perms as well.