Friday, August 15, 2008

I just started working with the new Entity Framework now that it has been released. One of the first tasks that I tackled was building an EF version of my LinqLength expression builder. Like my LinqLength expression builder, you can get a value for the MaxLength property from your schema:

<asp:TextBox ID="TextBox1" runat="server" Columns="50" MaxLength='<%$EntLength: Bellingham.Web.BellinghamEntities:BellinghamModel:Location:Name%>' />

I am still a little confused with all of relationships going on here and whether the model name is required to uniquely identify the entity given some database or conceptual models so I coded for both cases. If you only pass the expression builder three parameters, it will search across all models within the object context.

<asp:TextBox ID="TextBox1" runat="server" Columns="50" MaxLength='<%$EntLength: Bellingham.Web.BellinghamEntities:BelliLocation:Name%>' />

You will need to add this to your web.config file:

<expressionBuilders>
    <add expressionPrefix="EntLength" type="BinaryOcean.Web.Library.EntLengthExpressionBuilder" />
</expressionBuilders>

Also, I haven't completed thorough perfomance testing but do save the Items Collection for the csdl between calls in the HttpContext. I played with different caching methods and I think this one is likely the best given the different trade offs. Asp.net caches the result for each Expression Builder call so it is only called once during the application life. (It is cached across different user threads within IIS.) Bottom line, this is very much a work in progress.

-Andy

>>>>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.CodeDom;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Linq;
using System.Web;
using System.Web.Compilation;
using System.Web.UI;

namespace BinaryOcean.Web.Library
{
    [ExpressionPrefix("EntLength")]
    public class EntLengthExpressionBuilder : ExpressionBuilder
    {
        public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
        {
            string[] parameters = entry.Expression.Split(':');

            CodePrimitiveExpression expression = null;

            if (parameters.Count() == 3)
                expression = new CodePrimitiveExpression(GetFacetMaxLength(parameters[0], "", parameters[1], parameters[2]));

            if (parameters.Count() == 4)
                expression = new CodePrimitiveExpression(GetFacetMaxLength(parameters[0], parameters[1], parameters[2], parameters[3]));

            if (expression == null)
                ThrowException("Usage \"EntLength: EntityName[:ModelName]:TypeName:PropertyName\".");

            return expression;
        }

        private int GetFacetMaxLength(string contextName, string modelName, string entityName, string propertyName)
        {
            var entity = GetEntity(contextName, modelName, entityName);

            var property = entity.Properties.SingleOrDefault(p => p.Name == propertyName);
            if (property == null)
                ThrowException(string.Format("Unable to find a single Property with Name \"{0}\"", propertyName));

            var facet = property.TypeUsage.Facets.SingleOrDefault(f => f.Name == "MaxLength");
            if (facet == null)
                ThrowException("Unable to find Facet with Name \"MaxLength\"");

            if (!(facet.Value is int))
                ThrowException(string.Format("Unable to derive value from Facet \"MaxLength\" with Value \"{0}\" on Property Name \"{1}\"", facet.Value, propertyName));

            return (int)facet.Value;
        }

        private EntityType GetEntity(string contextName, string modelName, string entityName)
        {
            var itemCollection = GetItemCollection(contextName);

            EntityType entity;
            if (modelName.Length > 0)
            {
                entity = itemCollection.SingleOrDefault(i => i.NamespaceName == modelName && i.Name == entityName);
                if (entity == null)
                    ThrowException(string.Format("Unable to find a single Entity with Model Name \"{0}\" and Name \"{1}\"", modelName, entityName));
            }
            else
            {
                entity = itemCollection.SingleOrDefault(i => i.Name == entityName);
                if (entity == null)
                    ThrowException(string.Format("Unable to find a single Entity with Name \"{0}\"", entityName));
            }
            return entity;
        }

        private ReadOnlyCollection<EntityType> GetItemCollection(string contextName)
        {
            var dictionary = GetDictionary();

            if (dictionary.ContainsKey(contextName))
            {
                return dictionary[contextName];
            }

            var context = (ObjectContext)Activator.CreateInstance(BuildManager.GetType(contextName, true));
            var itemCollection = context.MetadataWorkspace.GetItemCollection(DataSpace.CSpace).GetItems<EntityType>();
            dictionary.Add(contextName, itemCollection);

            return itemCollection;
        }

        private const string DictionaryCacheKey = "177808CC-5099-4972-8113-F61137DC1875"; // use a random guid as a key to insure uniqueness
        private Dictionary<string, ReadOnlyCollection<EntityType>> GetDictionary()
        {
            var dictionary = HttpContext.Current.Items[DictionaryCacheKey] as Dictionary<string, ReadOnlyCollection<EntityType>>;

            if (dictionary == null)
            {
                dictionary = new Dictionary<string, ReadOnlyCollection<EntityType>>();
                HttpContext.Current.Items[DictionaryCacheKey] = dictionary;
            }

            return dictionary;
        }

        private void ThrowException(string message)
        {
            throw new InvalidOperationException(string.Format("EntLengthExpressionBuilder: {0}", message));
        }
    }
}

kick it on DotNetKicks.com   Friday, August 15, 2008 12:59:11 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  | 
 Monday, March 31, 2008

I use SourceGear Vault and generally love it. Guessing the following is not directly related to Vault but rather source control in general. By default, Vault is configured to ignore the Bin folder within an ASP.NET project or Web Site.

What have I been doing for years when I need to import a vendor's DLL to my project? Yup, I copy it to my Bin folder and add a reference to it. Wrong thing to do!

That 3rd party DLL is not achieved along with the rest of my project since Vault is configured to ignore the Bin folder. (Guessing that all source control systems follow this behavior.) What should be doing instead? Create a folder within my project along the lines of AcmeLibrary and copy DLLs there. Then reference my DLLs and Visual Studio will copy them to your Bin folder when you build your project. The AcmeLibrary and its contents will be placed under source control.

Hope this saves you some future grief!

kick it on DotNetKicks.com   Monday, March 31, 2008 6:53:51 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [2]  | 
 Friday, March 07, 2008

I find myself repeating the same little code pattern time after time when building a LINQ expression based on a number of optional filtering criteria. Using standard if statements seems a bit verbose to me.

if (SearchControlMain.CategoryID.HasValue)
    query = query.Where(q => q.CategoryID == SearchControlMain.CategoryID);

if (SearchControlMain.TypeID.HasValue)
    query = query.Where(q => q.TypeID == SearchControlMain.TypeID);

if (SearchControlMain.PostingID.HasValue)
    query = query.Where(q => q.PostingID == SearchControlMain.PostingID);


Completely inlining the if statements doesn't do much for readability.

if (SearchControlMain.CategoryID.HasValue) query = query.Where(q => q.CategoryID == SearchControlMain.CategoryID);
if (SearchControlMain.TypeID.HasValue) query = query.Where(q => q.TypeID == SearchControlMain.TypeID);
if (SearchControlMain.PostingID.HasValue) query = query.Where(q => q.PostingID == SearchControlMain.PostingID);


So I created a couple of quick extension methods on both the IEnumerable and IQueryable forms of Where. I named my new extension WhereIf but I struggled a bit as to whether to name it just Where and wind up with an overload.

Now you can code:

query = query.WhereIf(SearchControlMain.CategoryID.HasValue, q => q.CategoryID == SearchControlMain.CategoryID);
query = query.WhereIf(SearchControlMain.TypeID.HasValue, q => q.TypeID == SearchControlMain.TypeID);
query = query.WhereIf(SearchControlMain.PostingID.HasValue, q => q.PostingID == SearchControlMain.PostingID);


Here are the extension methods:

public static IEnumerable<TSource> WhereIf<TSource>(this IEnumerable<TSource> source, bool condition, Func<TSource, bool> predicate)
{
    if (condition)
        return source.Where(predicate);
    else
        return source;
}

public static IEnumerable<TSource> WhereIf<TSource>(this IEnumerable<TSource> source, bool condition, Func<TSource, int, bool> predicate)
{
    if (condition)
        return source.Where(predicate);
    else
        return source;
}

public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool condition, Expression<Func<TSource, bool>> predicate)
{
    if (condition)
        return source.Where(predicate);
    else
        return source;
}

public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool condition, Expression<Func<TSource, int, bool>> predicate)
{
    if (condition)
        return source.Where(predicate);
    else
        return source;
}

kick it on DotNetKicks.com   Friday, March 07, 2008 8:29:01 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [2]  | 
 Monday, February 25, 2008

An update on an older posting now that we have LINQ:

 

string[] input = { "1", "2", "3", "4", "5", "6", "7", "8", "9" };

 

// old

int[] output = Array.ConvertAll<string, int>(input, delegate(string s) { return int.Parse(s); });

 

// new

int[] output = Array.ConvertAll(input, s => int.Parse(s));

 

// even better

input.Select(s => int.Parse(s)).ToArray();

 

// better yet

input.Select(s => int.Parse(s)).ToList();

 

kick it on DotNetKicks.com   Monday, February 25, 2008 4:19:12 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  | 
 Sunday, February 24, 2008

When you use the LINQ to SQL design surface in Visual Studio, the generated code contains a Column Attribute which in turn contains a small piece of SQL text called the DbType. This property contains the column length for columns of type Char and VarChar. Using a bit of reflection, you can gain access to the value. Wrapping all of this within an ExpressionBuilder, you can easily set the MaxLength property of any TextBox declaratively in your web pages.

Here is the code for the Expression Builder:

using System;

using System.CodeDom;

using System.Data.Linq.Mapping;

using System.Linq;

using System.Reflection;

using System.Web.Compilation;

using System.Web.UI;

 

namespace BinaryOcean.Web.Utility

{

    [ExpressionPrefix("LinqLength")]

    public class LinqLengthExpressionBuilder : ExpressionBuilder

    {

        public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)

        {

            string typeName = BeforeLast(entry.Expression, ".");

            string propertyName = AfterLast(entry.Expression, ".");

 

            return new CodePrimitiveExpression(PropertyLength(typeName, propertyName));

        }

 

        private int PropertyLength(string typeName, string propertyName)

        {

            ColumnAttribute attribute =

                (ColumnAttribute)BuildManager

                .GetType(typeName, true)

                .GetProperty(propertyName)

                .GetCustomAttributes(typeof(ColumnAttribute), false)

                .Single(); // <-- look, I just had to use a tiny bit of LINQ in this code!

 

            return int.Parse(BetweenFirst(attribute.DbType, "char(", ")"));

        }

 

        private string BeforeLast(string value, string last)

        {

            return value.Substring(0, value.LastIndexOf(last));

        }

 

        private string AfterLast(string value, string last)

        {

            return value.Substring(value.LastIndexOf(last) + last.Length);

        }

 

        private string BetweenFirst(string value, string startText, string endText)

        {

            int start = value.IndexOf(startText, StringComparison.OrdinalIgnoreCase) + startText.Length;

            int end = value.IndexOf(endText, start,  StringComparison.OrdinalIgnoreCase);

 

            return value.Substring(start, end - start);

        }

    }

}

You must add the following bit of code to the system.web / compilation section of your web.config file. This loads the ExpressionBuilder which interprets your code.

 

<system.web>

  <compilation debug="true">

    <expressionBuilders>

      <add expressionPrefix="LinqLength" type="BinaryOcean.Web.Utility.LinqLengthExpressionBuilder"/>

    </expressionBuilders>

  </compilation>

</system.web>

 

The use of BuildManager.GetType() within the Expression Builder should allow you to reference dbml objects within you local AppCode folder or either local or external namespaces. Given that Name is a database column contained within the Employee table you can code this:

 

<asp:TextBox ID="TextBoxName" runat="server" MaxLength='<%$LinqLength:Employee.Name%>' />

 

If you are using Web Projects or if your data context is in a different namespace from your web application, you will have to prefix the above type with your namespace (and add dot ("."));

 

Enjoy!

kick it on DotNetKicks.com   Sunday, February 24, 2008 3:43:19 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  | 
 Monday, January 28, 2008

Love my new Zune, but I wasn't able to install the software. Received the following error:

Installation Failed
Setup must stop because the required package ‘Zune’ failed to install.
DIFXAPP: Rollback failed with error 0x2
Error code: 0x80070643
http://go.microsoft.com/fwlink/?LinkID=101253

Looking at the support boards, it seems that a number of people are having this issue. I did find a reference to an obscure registry hack that disabled the installer's ability to rollback after a failed install. This seemed to fix the issue but I tried upgrading to the new 2.3 version of the software which recently became available. No go. Infact, the upgrade process failed and broke my previously working software.

Yuck! I once again followed the link in the above error message. It was of zero help. On a lark, I tried re-enabling the Firewall Service that I had long ago disabled. Fixed! Seems that you can turn your firewall off, but can't disable the service. Guessing the install was trying to configure ports on the firewall.

Zune: Great product. Zune client software: Poor QA.

kick it on DotNetKicks.com   Monday, January 28, 2008 2:20:20 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [2]  | 
 Friday, September 28, 2007

I recently needed a dictionary for a small project. Nothing special there but on this occasion, I needed a dictionary that was keyed by two sub-keys. I thought about using an array but you loose strong typing. I thought about using  a dictionary of dictionaries but the code is messy, complicated and difficult to maintain. Here is a better solution. (I also coded structures that allow for compound keys with 3 and 4 values. If you need that, just expand on the 2 key version.):

 

[Serializable, StructLayout(LayoutKind.Sequential)]

public struct CompoundKey<T1, T2>

{

    private T1 key1;

    private T2 key2;

 

    public CompoundKey(T1 key1, T2 key2)

    {

        this.key1 = key1;

        this.key2 = key2;

    }

 

    public T1 Key1

    {

        get { return this.key1; }

    }

 

    public T2 Key2

    {

        get { return this.key2; }

    }

 

    public override string ToString()

    {

        StringBuilder builder = new StringBuilder();

        builder.Append('[');

 

        if (this.Key1 != null)

        {

            builder.Append(this.Key1.ToString());

        }

 

        builder.Append(", ");

 

        if (this.Key2 != null)

        {

            builder.Append(this.Key2.ToString());

        }

 

        builder.Append(']');

        return builder.ToString();

    }

}

 

My dictionary:

 

Dictionary<CompoundKey<int, string>, string> dictionary = new Dictionary<CompoundKey<int, string>, string>();

 

dictionary.Add(new CompoundKey<int, string>(1, "A"), "X123");

dictionary.Add(new CompoundKey<int, string>(2, "B"), "M394");

dictionary.Add(new CompoundKey<int, string>(3, "B"), "M395");

 

if (dictionary.ContainsKey(new CompoundKey<int, string>(3, "B")))

{

    Fred = dictionary[new CompoundKey<int, string>(3, "B")];

}

kick it on DotNetKicks.com   Friday, September 28, 2007 6:43:04 AM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [0]  | 
 Saturday, August 25, 2007
ASP.NET Tip: Occasionally I need to establish a line of communication between a User Control and my Master Page. Over the years, I have explored a few methods to accomplish this and seen more than a handful from others. The following method elegantly solves a number of problems for me and has become my standard coding practice.

First, a base class is needed for all Master Pages:

using System;

 

public abstract class BaseMaster : System.Web.UI.MasterPage

{

    public BaseMaster() { }

 

    public abstract void ShowMenu(bool visible);

    public abstract void SetStatusMessage(string message);

}

I define this class as abstract since it will never be instantiated directly. In addition, an abstract class allows us to include abstract methods and properties which in turn requires us to provides these methods on our actual Master Page.

My actual master page is coded as follows:

using System;

 

public partial class Main : BaseMaster

{

    protected void Page_Load(object sender, EventArgs e) { }

 

    public override void ShowMenu(bool visible)

    {

        MenuMain.Visible = visible;

    }

 

    public override void SetStatusMessage(string message)

    {

        LabelStatus.Text = message;

    }

}

Within a User Control, you can gain access to the Master Page via the Page.Master object. Using our base class (BaseMaster) you get a strongly typed object. You migh be tempted to cast the Page.Master object to the type of your Master Page but this limits you to a single Master Page. The base class method allows your site to implement multiple master pages and still access these common methods and properties.

using System;

 

public partial class WebUserControl : System.Web.UI.UserControl

{

    protected void Page_Load(object sender, EventArgs e) { }

 

    protected void ButtonUpdate_Click(object sender, EventArgs e)

    {

        BaseMaster master = Page.Master as BaseMaster;

 

        if (master == null)

            return;

 

        master.SetStatusMessage("Update Complete");

    }

}

One final consideration: your User Control should likely also derive from some type of base class. Within this base class provide matching properties and methods to those in your BaseMaster. This will simplify things for you by eliminating the need to recode the same methods and properties in each of your User Controls. If you are acessing these same methods from your Page, consider including methods in a page base class.

kick it on DotNetKicks.com   Saturday, August 25, 2007 8:28:37 PM (Pacific Standard Time, UTC-08:00)  #    Disclaimer  |  Comments [2]  |