15 Dec 2010

Castle ActiveRecord / NHibernate Update Event Interceptor to Log Dirty Property Changes

Man I love the Castle Project documentation. Or at least, I would if I was a robot with a brain the size of a planet and an unlimited amount of time on my hands. Sadly I'm a human and find the whole thing impenetrable.

All I wanted to do was to intercept the NHibernate entity update event so that I could log the changes made. With kudos to this dude and this dude, here's how I did it:

1. In my project create a new class:

using Castle;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Attributes;
using NHibernate;
using NHibernate.Event;

Note it uses a struct called EntityPropertyUpdate and a collection called RuntimeManager.EntityPropertyUpdates to keep a log of the changes. You'll have to roll your own if you want to do what I'm doing here, otherwise just modify the code as you see fit.

[EventListener()]
public class AuditUpdateListener : IPostUpdateEventListener
{
private const string _noValueString = "EMPTY";

private static string getStringValueFromStateArray(object[] stateArray, int position)
{
var value = stateArray[position];

return value == null || value.ToString() == string.Empty
? _noValueString
: value.ToString();
}

public void OnPostUpdate(PostUpdateEvent evt)
{
if (!(evt.Entity is Log_G))
{
var dirtyFieldIndexes = evt.Persister.FindDirty(evt.State, evt.OldState, evt.Entity, evt.Session);
foreach (var dirtyFieldIndex in dirtyFieldIndexes)
{
var oldValue = getStringValueFromStateArray(evt.OldState, dirtyFieldIndex);
var newValue = getStringValueFromStateArray(evt.State, dirtyFieldIndex);

if (oldValue != newValue)
RuntimeManager.EntityPropertyUpdates.Add(new RuntimeManager.EntityPropertyUpdate { EntityName = evt.Entity.GetType().Name, PropertyName = evt.Persister.PropertyNames[dirtyFieldIndex], PropertyFrom = oldValue, PropertyTo = newValue });
}
}
}
}


The other key thing is the ActiveRecordStarter.Initialize() call, which had to be changed to include the assembly containing the AuditUpdateListener class, which happened to be the main executing assembly. I was doing my AR Initialisation in global.asax so it now looks like this:


protected void Application_Start()
{
// ActiveRecord Setup
IConfigurationSource source = ActiveRecordSectionHandler.Instance;
Assembly asm = Assembly.Load("MyApp.DataLayer");
ActiveRecordStarter.Initialize(new Assembly[] {asm, Assembly.GetExecutingAssembly()}, source, new Type[] { });
}


That's all!

Incidentally though, if you want to use an ambient AR SessionScope like I do, add this to global.asax:

protected void Application_BeginRequest(Object sender, EventArgs e)
{
// Create a SessionScope for ActiveRecord
HttpContext.Current.Items.Add("ar.sessionscope", new SessionScope(FlushAction.Never));
}

protected void Application_EndRequest(object sender, EventArgs e)
{
// Close the SessionScope for ActiveRecord
try
{
SessionScope scope = HttpContext.Current.Items["ar.sessionscope"] as SessionScope;
if (scope != null)
scope.Dispose();
}
catch (Exception ex)
{
HttpContext.Current.Trace.Warn("Error", "EndRequest: " + ex.Message, ex);
}
}

6 Dec 2010

MVC File Upload - Using Uploadify with ASP.NET MVC 2 / 3


NOTE 23 Oct 2012

I no longer recommend the use of Uploadify with MVC. I've found benefit using javascript-only solutions such as DropZone : http://www.dropzonejs.com/

or Valum's File Uploader : https://github.com/Valums-File-Uploader/file-uploader




Uploadify is ace for performing file upload and image upload, but ASP.NET MVC docs were lacking, so I hope this helps someone. Critical points addressed here include:
  • Uploadify flash upload button is ugly so we use CSS to make it invisible and fake an HTML button under it.

  • The Uploadify flash upload doesn't pass the ASP.NET session and authentication cookies naturally, which we want for recognition and authorization, so we handle that.

UPLOADIFY FILES

Put all Uploadify files in a project folder called ClientScript/uploadify

SCRIPT

Put this code on the regular View page that performs the upload.

 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/jquery-1.4.2.min.js")  %>"></script>
 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/uploadify/swfobject.js") %>"></script>
 <script type="text/javascript" src="<%= Url.Content("~/ClientScript/uploadify/jquery.uploadify.v2.1.4.min.js")  %>"></script>
 
 <script type="text/javascript">

    $(function () {

        // Uploadify File Upload System
        // SessionSync data is sent in scriptData for security reasons, see UploadifySessionSync() in global.asax

        var UploadifyAuthCookie = '<% = Request.Cookies[FormsAuthentication.FormsCookieName] == null ? string.Empty : Request.Cookies[FormsAuthentication.FormsCookieName].Value %>'; 
        var UploadifySessionId = '<%= Session.SessionID %>';

        $("#fuFileUploader").uploadify({

            'hideButton'   : true,       // We use a trick below to overlay a fake html upload button with this hidden flash button                         
            'wmode'      : 'transparent', 
            'uploader': '<%= Url.Content("~/ClientScript/uploadify/uploadify.swf") %>',
            'cancelImg': '<%= Url.Content("~/ClientScript/uploadify/cancel.png") %>',
            'buttonText': 'Upload File',
            'script': '<%= Url.Action("FileUpload", "Media") %>',
            'multi': true,
            'auto': true,
            'scriptData' : { RequireUploadifySessionSync: true, SecurityToken: UploadifyAuthCookie, SessionId: UploadifySessionId },
            'onComplete' : function (event, ID, fileObj, response, data) 
                            {
                                response = $.parseJSON(response);
                                if (response.Status == 'OK')
                                {
         // Put your own handler code here instead of the following...
                                    alert('File Uploaded OK!');
                                }
                            }
        }); 
    });

 </script>

HTML

Put this HTML on the same View as the script above.

    <div style="position: relative;">
        
        <input type="button" id="btnUpload" value="Upload File" />

        <div style="position: absolute; top: 4px; left: 3px;">
            <input id="fuFileUploader" name="file_upload" type="file" />
        </div>
    </div>

GLOBAL.ASAX

Add the following bits.

 protected void Application_BeginRequest(Object sender, EventArgs e) 
 {
  if (HttpContext.Current.Request["RequireUploadifySessionSync"] != null)
   UploadifySessionSync();
 }

 /// <summary>
 /// Uploadify uses a Flash object to upload files. This method retrieves and hydrates Auth and Session objects when the Uploadify Flash is calling.
 /// </summary>
 /// <remarks>
 ///     Kudos: http://geekswithblogs.net/apopovsky/archive/2009/05/06/working-around-flash-cookie-bug-in-asp.net-mvc.aspx
 ///     More kudos: http://stackoverflow.com/questions/1729179/uploadify-session-and-authentication-with-asp-net-mvc
 /// </remarks>
 protected void UploadifySessionSync()
 {
  try
  {
   string session_param_name = "SessionId";
   string session_cookie_name = "ASP.NET_SessionId";

   if (HttpContext.Current.Request[session_param_name] != null)
    UploadifyUpdateCookie(session_cookie_name, HttpContext.Current.Request.Form[session_param_name]);
  }
  catch {}

  try
  {
   string auth_param_name = "SecurityToken";
   string auth_cookie_name = FormsAuthentication.FormsCookieName;

   if (HttpContext.Current.Request[auth_param_name] != null)
    UploadifyUpdateCookie(auth_cookie_name, HttpContext.Current.Request.Form[auth_param_name]);
  }
  catch {}
 }

 private void UploadifyUpdateCookie(string cookie_name, string cookie_value)
 {
  HttpCookie cookie = HttpContext.Current.Request.Cookies.Get(cookie_name);
  if (cookie == null)
   cookie = new HttpCookie(cookie_name);
  cookie.Value = cookie_value;
  HttpContext.Current.Request.Cookies.Set(cookie);
 } 

Models/MediaAssetUploadModel.cs
    public class MediaAssetUploadModel
    {
        public HttpPostedFileBase fileData { get; set; }
        public string SecurityToken { get; set; }
        public string Filename { get; set; }
    }

Controllers/MediaController.cs
 [Authorize]
 public ActionResult FileUpload(MediaAssetUploadModel uploadedFileMeta)
 {
  string fullFilePath = SaveUploadedFile(uploadedFileMeta);
  //TODO: Error handling
  return Json(new { Status = "OK" });
 }  

 //TODO: move this into a manager/repository class
 private string SaveUploadedFile(MediaAssetUploadModel uploadedFileMeta)
 {
  string fileName = Guid.NewGuid() + System.IO.Path.GetExtension(uploadedFileMeta.Filename);
  string fullSavePath = Path.Combine(ConfigurationManager.AppSettings["MediaAssetFolder"], fileName);
  uploadedFileMeta.fileData.SaveAs(fullSavePath);

  return fullSavePath;
 } 

Web.config

Make following changes to upload files to c:\temp , with a max file size of 100Mb.
Change these to suit yourself - c:\temp will get automatically cleared so you have been warned.
If you create your own upload folder, remember you will need to give write perms to the ASP.NET process.
 <configuration>
   <appSettings>
  <add key="MediaAssetFolder" value="c:\temp"/>
   </appSettings> 
   <system.web>
  <httpRuntime maxRequestLength="100000" /> 
   </system.web>
 </configuration>

26 Nov 2010

Simple Jquery Lightbox

Wrote some code that I didn't end up using, but it's a shame to bin it :)

Simple lightbox using Jquery UI dialog widget:

function LightBox(imgSrc, imgTitle)
{
var img = $('<img />');
$('body').append(img);
img.attr('src', imgSrc);
img.load(function()
{
var lightBox = $('<div></div>');
lightBox.dialog({ autoOpen: false, height: img.height() + 50, width: img.width() + 50, modal: true, title : imgTitle });
lightBox.html('').append(img);
lightBox.dialog('open');
});
};

12 Nov 2010

Duffs Lodge

We stayed at Duff's Lodge, Beaufort Estate nr. Beauly, Inverness-shire. Nice.

Would just like to say that the Priory Hotel in Beauly makes the worst sunday roast in the known world.

On the plus side, food and drink at The Cawdor Tavern is bloody lovely.

Apart from that, the road to Skye from Inverness is gobsmacking, and Glen Affric is beautiful.

That is all! Back to the C# shenanigans.

Beef Stew

Man make Stew! Stew good. Stew good for 3 people easy.

400g Beef cubes.
6 Mushrooms
4 Potatoes
1 Leek
1 Onion
3 Large carrots
600ml Beef Stock

Simmer tatties, leek and carrots for 15 mins.
Meanwhile, fry onion, beef and mushrooms for 10 mins.
Add stock to beef pan, then add the veggies.
Simmer the lot for 45 mins.

Nyom! Simmer longer and slower for added nyom.

28 Oct 2010

SharePoint 2010 Static Web Part to read Page Properties

• Create a new page property by creating a new Site Column
• Edit the Page content type and add the new Site column
• Now all pages will have the new property to edit
• Create your web part to read the Page Property, like

try
{
if (SPContext.Current.Fields.ContainsField("Your Page Property Name") && SPContext.Current.Item["Your Page Property Name"] != null)
myString = SPContext.Current.Item["Your Page Property Name"].ToString();
}
catch { }

• Install and Deploy the web part on the server
• Find the public key token of your part by looking it up under c:\windows\assemblies
• Register the web part as static in your masterpage or page layout:

<%@ Register Tagprefix="Applicable" Namespace="ApplicableWebPartLib.TwitterBlock" Assembly="ApplicableWebPartLib, version=1.0.0.0,Culture=neutral,PublicKeyToken=2ee629bf628499ee" %>

• Use the web part on your page layout or master page:

<Applicable:TwitterBlock runat="server"></Applicable:TwitterBlock>

27 Oct 2010

Documenting C# ASP,NET Code Projects and Solutions

We all know we should be using ///<summary> and ///<remark> tags in our code to document it properly, right?

The cool thing about that is that you can then generate an XML file from Visual Studio with all your code comments in it.
To do that, right-click on your Project in Solution Explorer and choose the "Build" tab. Check the box marked "XML documentation file" and voila - next time you build, the XML file will be generated too.

Then what? Then, my friend, you can distribute the xml file with your dll, and other developers will be able to see your comments on your class methods and properties.

Also, you can generate a Help file (CHM)! To do that, download and install Sandcastle and the SandCastle Help File Builder (SHFB).

Then in SHFB, you can just drag in your DLL and the XML file will be automatically included.
Build your Help File and a CHM will be generated!
Open it from a local drive (CHMs don't work on a remote drive) and enjoy!

20 Oct 2010

Getting Schema Information from SQL Server

As an aide-memoire to myself and anyone else who cares, here is a (growing, unfinished) list of the various ways you can get schema data and metadata out of SQL Server.

1. INFORMATION_SCHEMA Views

SELECT * FROM INFORMATION_SCHEMA.TABLES

SELECT * FROM INFORMATION_SCHEMA.COLUMNS


2. sys_objects

3. sp_tables, sp_columns

18 Oct 2010

Sharepoint 2010 Page Properties - Push Changes To Sites and Lists

I'm a bit confused, and it's no wonder. I'm trying to add a custom metadata field to the Page Properties of all the Article Pages on my Publishing Site, and all its subsites. However, when I made the change by creating a Site Column in Sharepoint 2010 Designer, and adding it to the Article Page Content Type column list, the column did not turn up on my subsite pages.

Then I noticed that when editing Content Type columns, there's a button in the ribbon marked "Push Changes to Sites and Lists". I can't find any documentation as to how this button works, or even if it is a button at all, as clicking it seems to make it glow yellow, as if it's a status indicator. Even spookier, at this moment a Google search for "Push Changes to Sites and Lists" comes up with ZERO results!

So, let's open this up for discussion! What is this button for? I *think* it has to be enabled (i.e. glowing yellow) when I make a column change, but BEFORE I press Save, in order for any lists or subsites using the Content Type to register the change.

1 Oct 2010

Delete a User from the ASP.NET membership database

Here's a sweet stored procedure to delete an ASP.NET user by only passing through a UserName, and not the 4 obscure arguments that the aspnet_Users_DeleteUser sproc requires.


CREATE PROCEDURE [dbo].[DeleteUser]
@UserName nvarchar(50)
AS
BEGIN

DECLARE @Error int;
DECLARE @UserGUID uniqueidentifier;


BEGIN TRAN

SELECT @UserGUID = UserID FROM aspnet_Users WHERE UserName = @UserName;
DELETE FROM aspnet_Profile WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_UsersInRoles WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM dbo.aspnet_PersonalizationPerUser WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_Membership WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;
DELETE FROM aspnet_users WHERE UserID = @UserGUID;
SET @Error = @@ERROR; IF @Error <> 0 GOTO ErrorHandling;

GOTO Final

ErrorHandling:

ROLLBACK TRAN
GOTO EndOfProc

Final:

COMMIT TRAN

EndOfProc:
SELECT @Error as Result
END

If I helped you out today, you can buy me a beer below. Cheers!