MvcSiteMap Provider BreadCrumbs

The below demo will show how to use MvcSiteMap Provider to do breadcrumbs in your MVC web project

Start with an empty MVC3 web application
image

image

There are a couple ways to get to MvcSiteMap
1. Using NuGet
2. Manually configuring MvcSiteMap Provider

If you use NuGet most of the work will be done for you, open the Package Manager Console and type in the following command and hit enter

PM> Install-Package MvcSiteMapProvider

The dll will be downloaded,MVC.SiteMap will be created, references will be added and a few web.config file entries will be made. If you wish to do it all by yourself then follow option 2

Manually configuring MvcSiteMap Provider

Download the dll from http://mvcsitemap.codeplex.com/

To your empty Mvc project add reference to MvcSiteMapProvider.dll by browsing to the location where you downloaded the dll.

image

Make sure CopyLocal is set to true, by default it is set to false.

image

Let us add few controllers and views to test out the breadcrumbs

Add HomeController

image

Add the view for Index Action

image

Update the Index.vbhtml to

Code Snippet
  1. @Code
  2. ViewData(“Title”) = “Home Index”
  3. End Code
  4. <h2>Home Index</h2>

Add another controller call it application

image

Add the view for Index action in the application controller

image

Update Application Index.vbhtml to look like below

Code Snippet
  1. @Code
  2. ViewData(“Title”) = “Application Index”
  3. End Code
  4. <h2>Application Index</h2>

We will add another action to the application controller, let us call it SpecialApp

Code Snippet
  1. Function SpecialApp(ByVal id As Integer) As ActionResult
  2. ViewBag.appId = id
  3. Return View()
  4. End Function

Add a view to the SpecialApp action as described before

Code Snippet
  1. @Code
  2. ViewData(“Title”) = “SpecialApp”
  3. End Code
  4. @ViewBag.appId
  5. <h2>SpecialApp</h2>

Let us add another controller called Account and then add Index view for this controller

Code Snippet
  1. @Code
  2. ViewData(“Title”) = “Accounts Page”
  3. End Code
  4. <h2>Acoounts Page</h2>

Now let us add the Mvc.sitemap

image

As you can see now I have 3 controllers and few views

image

But the way I want to arrange my website is as below

image

In order to achieve this let us modify the Mvc.sitemap to

Code Snippet
  1. <?xmlversion=1.0encoding=utf-8 ?>
  2. <mvcSiteMapxmlns=http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-3.0enableLocalization=true>
  3.   <mvcSiteMapNodetitle=Homecontroller=Homeaction=Index>
  4.     <mvcSiteMapNodetitle=Applicationcontroller=Applicationaction=Index>
  5.       <mvcSiteMapNodetitle=Special Applicationscontroller=Applicationaction=SpecialApp/>
  6.     </mvcSiteMapNode>
  7.       <mvcSiteMapNodetitle=Accounts Pagecontroller=Accountaction=Index/>
  8.   </mvcSiteMapNode>
  9. </mvcSiteMap>

Now to show the breadcrumbs you need to add a template view called SiteMapPathHelperModel.vbhtml, before that right click the project select properties and add MvcSiteMapProvider.web.html and add MvcSiteMapProvider.Web.Html.Models to the imported namespaces

image

Under the shared folder make a new folder called DisplayTemplates (I had big problems since I had space between Display and Templates so make sure there is not space) and then add a new view call it SiteMapPathHelperModel.vbhtml

Code Snippet
  1. @imports System.Web.Mvc.Html
  2. @imports System.Linq
  3. @imports MvcSiteMapProvider.Web.Html.Models
  4. @ModelType SiteMapPathHelperModel
  5. @For Each node In Model
  6. @Html.ActionLink(node.Title, Nothing, New With {.Controller = node.Controller, .action = node.Action})
  7. If node IsNot Model.Last() Then
  8. @<text> &gt; </text>
  9. End If
  10. Next

Here for each node I make an html actionLink, then add a > sign except for the last node
Now in order to display the breadcrumbs lets edit the _Layout.vbhtml,
Add @imports MvcSiteMapProvider.Web.Html

Change

Code Snippet
  1. <body>
  2. @RenderBody()
  3. </body>

To

Code Snippet
  1. <body>
  2. <p>Breadcrumbs:</p>
  3. @Html.MvcSiteMap().SiteMapPath()
  4. @RenderBody()
  5. </body>

Now when you run the application you will see the error screen “The file web.sitemap required by XmlSiteMapProvider does not exist”.
That is because we have still not told the Framework what SiteMapProvider we are using. In order to do that open the application web.config file and place the below code inside system.web tag

Code Snippet
  1. <siteMapdefaultProvider=MvcSiteMapProviderenabled=true>
  2. <providers>
  3. <clear />
  4. <addname=MvcSiteMapProvidertype=MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvidersiteMapFile=~/Mvc.SitemapsecurityTrimmingEnabled=truecacheDuration=5enableLocalization=truescanAssembliesForSiteMapNodes=trueincludeAssembliesForScan=“”excludeAssembliesForScan=“”attributesToIgnore=visibilitynodeKeyGenerator=MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvidercontrollerTypeResolver=MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvideractionMethodParameterResolver=MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvideraclModule=MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvidersiteMapNodeUrlResolver=MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvidersiteMapNodeVisibilityProvider=MvcSiteMapProvider.DefaultSiteMapNodeVisibilityProvider, MvcSiteMapProvidersiteMapProviderEventHandler=MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider />
  5. </providers>
  6. </siteMap>

When you run the application you will see, the breadcrumbs show up
image
image

Using attributes

MvcSiteMap Provider also as the ability to mark the action methods with attributes to link up the sitemap. Let us say I have an action called HomeApp in the application controller

Code Snippet
  1. Function HomeApp(ByVal id As Integer) As ActionResult
  2. ViewBag.appId = id
  3. Return View()
  4. End Function

but I want this link to show up after the home index in the breadcrumbs. What I need to do is add a key to the home node as below

Code Snippet
  1. <mvcSiteMapNodetitle=Homecontroller=Homeaction=Indexkey=Home1>

Then add attributes to the action method

Code Snippet
  1. <MvcSiteMapNode(Title:=“Home Applications”, ParentKey:=“Home1”)>
  2. Function HomeApp(ByVal id As Integer) As ActionResult
  3. ViewBag.appId = id
  4. Return View()
  5. End Function

You will have to add Imports MvcSiteMapProvider at the top of the controller
Now when you run the app
image

Concurrency in Entity Framework 4.1 code first.

Let us assume we have a person entity and it looks as below

Imports System.ComponentModel.DataAnnotations
Public Class Person

    Public Property ID As Integer
    <StringLength(50)>
    Public Property FirstName As String
    <Timestamp()>
    Public Property RowVersion As Byte()

End Class

Using EF 4.1 and code first, a typical update would look as show below

       Using entities = New EFConcurrencyTestContext
            Try
                Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
                personEntity.FirstName = "CCC"
                entities.SaveChanges()
            Catch concurrencyException As DbUpdateConcurrencyException
                Throw New Exception("The record u r trying to update has changed")
            Catch ex As Exception
                Throw New Exception("Some Exception")

            End Try
        End Using
Since our main focus is to test Concurrency, let put a break point at the line Highlighted, start debugging when the debugger stops at personEntity.FirstName = "CCC", in the database update the person firstname, then continue debugging, on the savechanges you would get a DbUpdateConcurrencyException. This is pretty straight forward and works as expected. Let’s take a different scenario, for e.g. in multi tier MVC application

1.    In the service layer I would do get person, say the person FirstName is ‘A’, RowVersion is 1
2.    I would then build a ViewModel, take only a few data elements from person entity I need for the viewModel, I don’t transfer the person entity across tiers, meaning my controller only know about my viewmodel and is unaware of my domain object which is person. In order to check for Concurrency I will create a property in my ViewModel called PersonRowVersion and store the rowversion in it.
3.    As the data is being manipulated by my view, let us assume the record in the database was updated as well.  Say FirstName in the Database is ‘B’ and RowVersion is 2
4.    Now my View modified the person FirstName to ‘C’, I had stored my RowVersion either using a hidden field or in the Tempdata. I build the ViewModel, take the modified FirstName which is ‘C’ and restore the RowVersion from my hidden field which is 1
5.    In the service layer, I get the person entity from the database, which will have the FirstName = ‘B” and RowVersion = “2”. I update the FirstName to “C” and RowVersion to “1” and do Context.Savechanges. The update happens but no concurrency exception is raised
6.    In order to get the Concurrency Exception you will have to modify the OriginalValue of the property that is used for tracking concurrency, in my case it is RowVersion. Something like this

entities.Entry(personEntity).Property("RowVersion").OriginalValue = _rowVersion

I have created a few test cases (Windows client application) to mimic this process. The idea is to show what works and what does not.

Imports System.Data.Entity.Infrastructure

Public Class Form1

    Private _fName As String
    Private _rowVersion As Byte()
    Private _person As Person

    Private Sub CreateDB_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CreateDB.Click
        Using entities = New EFConcurrencyTestContext
            Dim personEntity As New Person With {.FirstName = "AAA"}
            entities.People.Add(personEntity)
            entities.SaveChanges()
        End Using
    End Sub

    Private Sub Get1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Get1.Click
        Using entities = New EFConcurrencyTestContext
            Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
            _fName = personEntity.FirstName
            _rowVersion = personEntity.RowVersion
            _person = personEntity
        End Using
    End Sub

    Private Sub TestConcurrencyWrongWayButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyWrongWayButton.Click
        Using entities = New EFConcurrencyTestContext
            Try
                Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
                personEntity.FirstName = "CCC"
                personEntity.RowVersion = _rowVersion
                entities.SaveChanges()
            Catch concurrencyException As DbUpdateConcurrencyException
                Throw New Exception("The record u r trying to update has changed")
            Catch ex As Exception
                Throw New Exception("Some Exception")

            End Try

        End Using
    End Sub

    Private Sub TestConcurrencyRightWayButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyRightWayButton.Click
        Using entities = New EFConcurrencyTestContext
            Try
                Dim personEntity = entities.People.Where(Function(p) p.ID = 1).FirstOrDefault
                personEntity.FirstName = "CCC"
                entities.Entry(personEntity).Property("RowVersion").OriginalValue = _rowVersion
                entities.SaveChanges()
            Catch concurrencyException As DbUpdateConcurrencyException
                Throw New Exception("The record u r trying to update has changed")
            Catch ex As Exception
                Throw New Exception("Some Exception")
            End Try

        End Using
    End Sub

    Private Sub TestConcurrencyWithAttachButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TestConcurrencyWithAttachButton.Click
        Using entities = New EFConcurrencyTestContext
            Try
                _person.FirstName = "xxxx"
               entities.Entry(_person).State = EntityState.Modified
                entities.SaveChanges()
            Catch concurrencyException As DbUpdateConcurrencyException
                Throw New Exception("The record u r trying to update has changed")
            Catch ex As Exception
                Throw New Exception("Some Exception")
            End Try
        End Using
    End Sub

    Private Sub ModifiedRepositoryWithOriginalRowVersionButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ModifiedRepositoryWithOriginalRowVersionButton.Click
        Dim _repository As New TestRepository(New EFConcurrencyTestContext)
        Try
            Dim personEntity = _repository.GetQuery(Of Person).Where(Function(p) p.ID = 1).FirstOrDefault
            personEntity.FirstName = "CCC"
           _repository.SetRowVersion(Of Person)(personEntity, _rowVersion)
            _repository.Save()
        Catch concurrencyException As DbUpdateConcurrencyException
            Throw New Exception("The record u r trying to update has changed")
        Catch ex As Exception
            Throw New Exception("Some Exception")

        End Try
    End Sub
End Class

image

Test Case 1
1.    Click Get 1 – Person data is got from the database and stored in memory
       a.    Person.FirstName = A
       b.    Person.RowVersion = 1
2.    Update the Person Table FirstName = B, the row version will be 2
3.    Click TestConcurrencyWorngWay, Update happens no concurrency exception

Test Case 2
Repeat 1 and 2 as in Test Case1, Click TestConcurrencyRightWay. No Update, concurrency exception is raised

Test Case 3

You can also get the concurrency exception to work by doing attach, but you got to store the person entity in memory to attach it to the context. In multi tier MVC application if you are using View model to carry data to the controller, then this becomes a problem as you need to store the domain object in memory. But to test it repeat 1 and 2 as in test case 1 and then click TestConcurrencyWith Attach, no update happens concurrency exception occurs

Test Case 4

Sometime you may be using repository pattern in your application, to test this repeat 1 and 2 as in test case 1, and then click ModifiedRepositoryWithOriginalRowVersion. Below is a sample repository

Imports System.Data.Entity
Imports System.Data.Entity.Design.PluralizationServices
Imports System.Globalization
Imports System.Data.Entity.Infrastructure

Public Class TestRepository
    Private _context As DbContext
    Private ReadOnly _pluralizer As PluralizationService = PluralizationService.CreateService(CultureInfo.GetCultureInfo("en"))
    Public Sub New(ByVal context As DbContext)
        If context Is Nothing Then
            Throw New ArgumentNullException("context")
        End If
        Me._context = context
    End Sub

    Private Function GetEntityName(Of TEntity As Class)() As String
        Return String.Format("{0}.{1}", DirectCast(_context, IObjectContextAdapter).ObjectContext.DefaultContainerName, _pluralizer.Pluralize(GetType(TEntity).Name))
    End Function

    Public Sub Save()
        _context.SaveChanges()
    End Sub

    Public Sub SetRowVersion(Of TEntity As Class)(ByVal entity As TEntity, ByVal originalRowVersion As Byte())
       _context.Entry(entity).Property("RowVersion").OriginalValue = originalRowVersion
    End Sub

    Public Function GetQuery(Of TEntity As Class)() As IQueryable(Of TEntity)
        Dim entityName = GetEntityName(Of TEntity)()
        Return DirectCast(_context, IObjectContextAdapter).ObjectContext.CreateQuery(Of TEntity)(entityName)
    End Function
End Class