clausn.dk

in my mind

WebformsMvp – Part 2

clock July 28, 2010 10:06 by author cln

In the first blog post, we covered the basics of creating a user control with WebformsMVP. In this blog post we’ll look at another example: Deletion of a news item. The example might be simple to understand, but there is some logic to cover. Our frontpage displayed a list of news items with respective edit and delete links, which only link to another page. From EditNewsItem.aspx we can edit a news item and from DeleteNewsItem.aspx we can delete a news item – it’s a confirmation page…

Remember: Presenter first

Our first step is to create our presenter: “NewsItemDeletePresenter”:

public class NewsItemDeletePresenter : Presenter<INewsItemDelete>

The difference to our first presenter is the generic parameter for the base presenter class, in this case an interface called “INewsItemDelete”, which looks like this:

public interface INewsItemDelete : IView<NewsItem>
{
    event Action<NewsItem> NewsItemDeleting;
}

This interface inherits from “IView<NewsItem>”, which would be a normal approach to using presenters. We want our model to be a “NewsItem”, but we also want a possibility to act when a button is clicked to delete the news item. Hence we add an event to the interface between the presenter and view, called “NewsItemDeleting”. The type of event is an “Action<NewsItem>”, which gives us a prototype of a receiving method like so:

void View_NewsItemDeleting(NewsItem newsItem)

So, creating our constructor looks like this:

public NewsItemDeletePresenter(INewsItemDelete view, IData data, ILogging logging)
    : base(view)
{
    _data = data;
    _logging = logging;
 
    View.Load += View_Load;
    View.NewsItemDeleting += View_NewsItemDeleting;
}
 
public override void ReleaseView()
{
    View.Load -= View_Load;
    View.NewsItemDeleting -= View_NewsItemDeleting;
}

We set our data and logging variables and listen to both the view’s “Load” and “NewsItemDeleting” events. The “ReleaseView” is also shown here, since it is rather simple – we clean up our event subscriptions.

Next we implement the 2 methods for our event subscription. The “View_Load” first.

Load

void View_Load(object sender, EventArgs e)
{
    Guid newsId;
    if (!Guid.TryParse(this.Request["id"], out newsId))
    {
        this.Response.Redirect("default.aspx", true);
        return;
    }
 
    var newsItem = _data.GetNewsItem(newsId); 
    if (newsItem == null)
    {
        this.Response.Redirect("default.aspx", true);
        return;
    }
    View.Model = newsItem as NewsItem;
}

Our first step is to retrieve the id from the query string and parse it to a valid “Guid” object. If this is not possible, we cannot find it in the data layer. This means we should redirect back to “default.aspx”, if no valid id can be found.

What you’ll notice here is the “this.Request” and “this.Response” property provided by base Presenter class. You have access to both “Cache”, “HttpContext”, “RouteData”, “Request”, “Response”, “Server” and a couple other properties, which enables you to get URL strings and info, redirect, get server values, etc. What’s missing is the “Session” property, which is on purpose. The argument being from the framework builder’s is, that sessions don’t scale (another discussion, but not in this blog series).

If we get a valid “Guid” from the url parameter “id”, we retrieve the news item from the data layer and check if it returns a “null”-value. If the value is “null”, we also redirect back, because a valid “Guid” id could be passed to the page, but that does not mean it exist in the data layer.

Lastly we set the views mode to our retrieved news item.

Load Test

We will not be performing a load test, but we will test the load method!

Our first goal is to test the first portion of the method, which checks for a valid “Guid” via the “this.Requst” property. This test shows the power of WebformsMVP, since it allows us to abstracts the webserver away. With Rhino, we can mock the “HttpContext”, “HttpRequest” and “HttpResponse” like so (We are still using the Arrange, Act and Assert (AAA) method):

[TestMethod]
public void ViewLoadParsesIdButCannotParseGuidFromRequest()
{
    // Arrange
    var view = MockRepository.GenerateStub<INewsItemDelete>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
    var httpContext = MockRepository.GenerateMock<HttpContextBase>();
    var httpRequest = MockRepository.GenerateMock<HttpRequestBase>();
    var httpResponse = MockRepository.GenerateMock<HttpResponseBase>();
 
    var requestEntry = "id";
    var requestValue = "sdf34rsdfsdgcfg3j";
    var redirectUrl = "default.aspx";
 
    httpRequest.Stub(r => r[requestEntry]).Return(requestValue);
    httpResponse.Expect(r => r.Redirect(redirectUrl, true));
    httpContext.Stub(h => h.Request).Return(httpRequest);
    httpContext.Stub(h => h.Response).Return(httpResponse);
 
    var presenter = new NewsItemDeletePresenter(view, data, logging)
    {
        HttpContext = httpContext
    };
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    presenter.ReleaseView();
 
    // Assert
    httpResponse.VerifyAllExpectations(); // Expect redirect
}

We setup view, data and logging, and we setup both “HttpContext”, “HttpRequest” and “HttpResponse” using a mock of the base classes “HttpContextBase”, “HttpRequestBase” and “HttpResponseBase”. These classes are provided by the .NET framework in “System.Web.Abstractions”. We will see their use in just a minute.

Next we define our request entry and value and redirect URL. This is to simplify the reading of the code, but it also makes it possible in other scenarios to assert what actually happened.

Lastly we arrange what should happen, so we create the “httpRequest” mock, and instruct it to return our “requestValue”, if someone accesses the entry “requestEntry”. This means that for future references of “httpRequest[‘id’]”, the object returns “sdf34rsdfsdgcfg3j”.
For the “httpResponse” object, we expect that the redirect method is going to be called with the “redirectURL” and “true” as parameters, because that the presenter cannot validate the “requestValue” as a “Guid”.
Last we setup the “httpContext” to return “httpRequest” and “httpResponse” when accessing their respective properties on the object.

We can now create our presenter, and set is “HttpContext” to the mocked context we just created.

Now we “replay” the scenario by raising the view’s load event and supplying the parameters. Afterwards we release the view and we can assert our expectations.
In this scenario, we only expect the “httpResponse” object to have its “Redirect” method called with certain properties. If this does not happen, the test will fail.

The next test tests almost the same thing when retrieving a news item from the datalayer. We expect a redirect if no news item can be found, but I won’t show this here, you can look in the code.

What’s more interesting is that I have actually not coded the method very well. Take a look at this test:

[TestMethod]
public void ViewLoadParsesIdButDataLayerThrowsException()
{
    // Arrange
    var view = MockRepository.GenerateStub<INewsItemDelete>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
    var httpContext = MockRepository.GenerateMock<HttpContextBase>();
    var httpRequest = MockRepository.GenerateMock<HttpRequestBase>();
    var httpResponse = MockRepository.GenerateMock<HttpResponseBase>();
 
    var requestEntry = "id";
    var requestValue = "E4B202593D9E4D8EA16A02091EBC439F";
    var redirectUrl = "default.aspx";
    var newsId = Guid.Parse(requestValue);
    var dataLayerException = new Exception();
 
    data.Stub(d => d.GetNewsItem(newsId)).Throw(dataLayerException);
 
    httpRequest.Stub(r => r[requestEntry]).Return(requestValue);
    httpResponse.Expect(r => r.Redirect(redirectUrl, true));
    httpContext.Stub(h => h.Request).Return(httpRequest);
    httpContext.Stub(h => h.Response).Return(httpResponse);
 
    var presenter = new NewsItemDeletePresenter(view, data, logging)
    {
        HttpContext = httpContext
    };
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    presenter.ReleaseView();
 
    // Assert
    httpResponse.VerifyAllExpectations(); // expect redirect
}

It’s almost the same, but includes a call to the data layer, where it throws an exception. This gives me the following error message when running the test: “WebformsMvpDemo.Logic.Tests.NewsItemDeletePresenterTests.ViewLoadParsesIdButDataLayerThrowsException threw exception: ...”
We better “try-catch” the call to the data layer – new “View_Load” method:

void View_Load(object sender, EventArgs e)
{
    Guid newsId;
    if (!Guid.TryParse(this.Request["id"], out newsId))
    {
        this.Response.Redirect("default.aspx", true);
        return;
    }
 
    NewsItem newsItem = null;
    try
    {
        newsItem = _data.GetNewsItem(newsId); // Do not set View.Model directly if you test for View.Model == null. When View.Model is null the framwork throws an exception
    }
    catch (Exception ex)
    {
        newsItem = null;
        _logging.Write(string.Format("Could not retrieve news item for id: '{0}'.", newsId.ToString("N")), ex);
    }
     
    if (newsItem == null)
    {
        this.Response.Redirect("default.aspx", true);
        return;
    }
    View.Model = newsItem;
}

Much better, and the test does not fail!

The last test before user interaction, where everything works (a sunshine scenario), will not be written here – look in the code Smile.

Deletion of news item

Next up, the “View_NewsItemDeleting” method. Our event prototype defines a method with a single parameter, a “NewsItem”. We need to implement a method that handles the deletion of the particular news item, and then redirect back to the news list:

void View_NewsItemDeleting(NewsItem newsItem)
{
    try
    {
        _logging.Write(string.Format("Deleting news item with id: '{0}'.", newsItem.Id.ToString("N")));
        _data.DeleteNewsItem(newsItem);
        _logging.Write(string.Format("News item with id: '{0}', was deleted.", newsItem.Id.ToString("N")));
    }
    catch (Exception e)
    {
        _logging.Write(string.Format("Could not delete news item with id: '{0}'", newsItem.Id.ToString("N")), e);
    }
    this.Response.Redirect("default.aspx");
}

This method could do some more stuff, like showing an error message if the news item cannot be deleted etc., but for simplicity the method is kept short.

Delete test

The test is rather straightforward and is composed of 2 test methods. The first presented is where the data layer throws an exceptions:

[TestMethod]
public void ViewUpdatesNewsButDataLayerTrowsException()
{
    // Arrange
    var view = MockRepository.GenerateStub<INewsItemDelete>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
    var httpContext = MockRepository.GenerateMock<HttpContextBase>();
    var httpRequest = MockRepository.GenerateMock<HttpRequestBase>();
    var httpResponse = MockRepository.GenerateMock<HttpResponseBase>();
 
    var requestEntry = "id";
    var requestValue = "4c278cea5c2145d395a64a9188d960c4";
    var redirectUrl = "default.aspx";
    var newsId = Guid.Parse(requestValue);
    var newsItemFromDb = new NewsItem { Id = newsId, Date = DateTime.Now, Title = "News item 1", Content = "Hello world" };
    var newsItemFromView = new NewsItem { Id = newsId, Date = DateTime.Now, Title = "News item 1-2", Content = "Hello world3" };
    var exception = new Exception();
    var loggingStartMessage = string.Format("Deleting news item with id: '{0}'.", newsItemFromView.Id.ToString("N"));
    var loggingExceptionMessage = string.Format("Could not delete news item with id: '{0}'", newsItemFromView.Id.ToString("N"));
 
    data.Stub(d => d.GetNewsItem(newsId)).Return(newsItemFromDb);
    data.Stub(d => d.DeleteNewsItem(newsItemFromView)).Throw(exception);
    logging.Expect(l => l.Write(loggingStartMessage));
    logging.Expect(l => l.Write(loggingExceptionMessage, exception));
    httpRequest.Stub(r => r[requestEntry]).Return(requestValue);
    httpResponse.Expect(r => r.Redirect(redirectUrl));
    httpContext.Stub(h => h.Request).Return(httpRequest);
    httpContext.Stub(h => h.Response).Return(httpResponse);
 
    var presenter = new NewsItemDeletePresenter(view, data, logging)
    {
        HttpContext = httpContext
    };
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    view.Raise(v => v.NewsItemDeleting += null, newsItemFromView);
    presenter.ReleaseView();
 
    // Assert
    httpResponse.VerifyAllExpectations(); // expect redirect
    logging.VerifyAllExpectations(); // expect logging
}

We use the items from arrange again and add an exception and logging message, since the datalayer throws an exception when calling “DeleteNewsItem”.

In the act-part, we raise both the views “load” and “NewsItemDeleting” events, to initiate the deletion of the news item.

Lastly we assert that a redirect has been started and that the logging has occurred.

A success scenario is described in the last test:

[TestMethod]
public void ViewDeletesNewsAndSucceeds()
{
    // Arrange
    var view = MockRepository.GenerateStub<INewsItemDelete>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
    var httpContext = MockRepository.GenerateMock<HttpContextBase>();
    var httpRequest = MockRepository.GenerateMock<HttpRequestBase>();
    var httpResponse = MockRepository.GenerateMock<HttpResponseBase>();
 
    var requestEntry = "id";
    var requestValue = "4c278cea5c2145d395a64a9188d960c4";
    var redirectUrl = "default.aspx";
    var newsId = Guid.Parse(requestValue);
    var newsItemFromDb = new NewsItem { Id = newsId, Date = DateTime.Now, Title = "News item 1", Content = "Hello world" };
    var loggingDebugBeginMessage = string.Format("Deleting news item with id: '{0}'.", newsItemFromDb.Id.ToString("N"));
    var loggingDebugEndMessage = string.Format("News item with id: '{0}', was deleted.", newsItemFromDb.Id.ToString("N"));
 
    data.Stub(d => d.GetNewsItem(newsId)).Return(newsItemFromDb);
    logging.Expect(l => l.Write(loggingDebugBeginMessage));
    logging.Expect(l => l.Write(loggingDebugEndMessage));
    httpRequest.Stub(r => r[requestEntry]).Return(requestValue);
    httpResponse.Expect(r => r.Redirect(redirectUrl));
    httpContext.Stub(h => h.Request).Return(httpRequest);
    httpContext.Stub(h => h.Response).Return(httpResponse);
 
    var presenter = new NewsItemDeletePresenter(view, data, logging)
    {
        HttpContext = httpContext
    };
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    view.Raise(v => v.NewsItemDeleting += null, newsItemFromDb);
    presenter.ReleaseView();
 
    // Assert
    httpResponse.VerifyAllExpectations(); // Expect redirect
    logging.VerifyAllExpectations(); // verify logging messages
}

We now have tests that verify what we are doing in the presenter is valid. Next step is to create the actual UserControl – create a new UserControl called “NewsItemDelete” and open it’s code behind file. Here we change the inheritance from a “UserControl” to “MvpUserControl<NewsItem>” and also defines that we want to implement our extended view interface in “INewsItemDelete”. This gives us an implementation of our UserControl like so:

[PresenterBinding(typeof(NewsItemDeletePresenter))]
public partial class NewsItemDelete : MvpUserControl<NewsItem>, INewsItemDelete
{
    protected void DeleteNewsItem(object sender, EventArgs e)
    {
        if (NewsItemDeleting != null)
        {
            NewsItemDeleting(this.Model);
        }
    }
 
    #region INewsItemDelete Members
 
    public event Action<NewsItem> NewsItemDeleting;
 
    #endregion
}

Again, the “PresenterBinding”-attribute should be overlooked for now, but we assign a “NewsItemDeletePresenter” to use as our presenter. The “DeleteNewsItem” is an eventhandler for a “Button” in the markup:

<table cellpadding="2" cellspacing="0">
    <tr>
        <td>Id:</td>
        <td><%
   1: : this.Model.Id.ToString("N") 
%></td>
    </tr>
    <tr>
        <td>Title:</td>
        <td><%
   1: : this.Model.Title 
%></td>
    </tr>
    <tr>
        <td>Date:</td>
        <td><%
   1: : this.Model.Date.ToString("dd/MM-yyyy") 
%></td>
    </tr>
    <tr>
        <td>Content:</td>
        <td><%
   1: : this.Model.Content 
%></td>
    </tr>
    <tr>
        <td>&nbsp;</td>
        <td><asp:Button ID="btnDeleteNewsItem" runat="server" OnClick="DeleteNewsItem" Text="Delete news item" /></td>
    </tr>
</table>

The markup defines a table presenting the news item about to be deleted, and includes the button in the bottom row. The button has an “OnClick” event called “DeleteNewsItem” presented in the last code block.

When a user clicks the button, the UserControls raises the event “NewsItemDeleting”, if anyone listens to it. If our tests are valid, our presenter should listen to it and we have a loose coupling of logic and interaction (Disregarding the attribute, I know!).

Outro

We’ve created a simple interaction with a XML file that holds our news items. So far we can list and delete news items, but the code also contains the ability to create and edit news items. Feel free to look in the code for examples.

Get the code here.



WebformsMVP – Part 1

clock July 28, 2010 09:24 by author cln

Introduction

When developing projects with ASP.NET it can be a more or less troublesome experience to test the solution being generated. What it comes down to is separation of concerns.

Developing with Webforms in ASP.NET, it allows you to define your controls and layout (possibly via CSS) using markup and events can be hooking in a code behind file. This setup might work for a lot of cases, but it is still very hard to test and you have your logic combined with your presentation.

Introducing: WebformsMVP!

WebformsMVP is a framework for helping achieve separation of your logic, data and view. The name is short for (ASP.NET) Webforms Model-View-Presenter, where the Model is your data model (i.e. a database), the View is your user controls and the Presenter is your logic. User controls define how data is displayed, and the Presenters handle the logic of interacting with the model:

ModelViewPresenter

Two important parts are the communication between View and Presenter, and Presenter and Model. What’s more important, is the lack of communication between View and Model!

Compared to Model-View-Controller the View is a bit more responsible than a normal view, in the sense that user interaction occurs in the view and the initiation also happens in the view. What Model-View-Presenter adds, is the decupling of the presenter from the button clicks etc., but it still knows about ASP.NET.

Enough introduction, let’s learn how you use it!

Open Visual Studio (I’m using 2010), and set up the project with appropriate names in the following structure (Web application, class library and MS Test project respectively):
image001

The sample project

The project I’m building is a news list creator. The purpose of the project is to list news, create, edit and delete them. I’m using a simple XML file for data, and using LINQ wherever possible. Testing is done with help from Rhino Mocks

Initial steps:

· Add WebformsMvp.dll to all projects

· Add a reference to Rhino.Mocks, System.Web and System.Web.Abstractions in the test project

In the following examples I’ll be using a class “NewsItem” with the following properties:

public class NewsItem
{
    public Guid Id { get; set; }
    public DateTime Date { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}

The class is placed in a subfolder in the Logic project under “Views” called “Models”, where the namespace is WebformsMvpDemo.Logic.Views.Models. The namespace will make sense when you start using the framework for more advances scenarios.

Create the presenter first

We want to create page with a UserControl that displays all the news items created in our XML file, and create respective links to edit and delete. Just above the list, we insert a link to create a new news item.

To create our UserControl, we first create our presenter (It will all make sense in the end J). In the project WebformsMvpDemo.Logic, create a folder called “Presenters” and create a normal class called “NewsListPresenter”. This class should extend the class WebFormsMvp.Presenter with the generic parameter “IView<List<NewsItem>>”, like so:

public class NewsListPresenter : Presenter<IView<List<NewsItem>>>

In order to extend Presenter<T>, you have to implement the “ReleaseView”-method. This method will make sense in just a minute.

Next step is to implement a constructor for the “NewsListPresenter”, because the base class does not have a constructor with 0 parameters. The parameter type for the base constructor is the generic type of “IView<T>”, and in our case it is “List<NewsItem>”:

public NewsListPresenter(IView<List<NewsItem>> view, IData data, ILogging logging) : base(view)
{
    _data = data;
    _logging = logging;
 
    View.Load += View_Load;
}

I’ll explain the last parameters in the next blog post, but for now assume that some magic happens and the correct “IData” and “ILogging” parameters are passed.

The constructor code sets the local variables for a data interface (i.e. your model), a logging interface and subscribes to the views “Load” -event. This load event is available through the WebformsMVP framework, and their argument being that most data is retrieved in the load method of a Page, UserControl, etc. Next we implement the “View_Load” –method:

void View_Load(object sender, EventArgs e)
{
    try
    {
        View.Model = _data.GetNewsItems();
    }
    catch (Exception ex)
    {
        View.Model = new List<NewsItem>();
        _logging.Write("Could not get news items from data layer", ex);
    }
}

When the view loads, we want to retrieve the data to be displayed and catch any errors that might occur. It’s important to keep the views model initialized or an exception is going to be thrown when accessing it. In the catch of the data retrieval, we set the views model to en empty list and write an error to the logging framework. The “ReleaseView”-method simply releases our subscription to the views load event.

We are done implementing the presenter – next we have to test it!

Test

Wait, what? Yes, you read it right: Test Smile

I’m going to be using Rhino Mocks to help with the tests. When using Rhino, you create a test case via the Arrange, Act and Assert (AAA) methodology. We first arrange our variables, dependencies and our expectations of the test, then we act to “play” the scenario and finally we assert our expectations:

[TestMethod]
public void NewsListPresenterGetsNewsItemsFromModel()
{
    // Arrange
    var view = MockRepository.GenerateStub<IView<List<NewsItem>>>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
 
    data.Stub((d) => d.GetNewsItems()).Return(new List<NewsItem>());
 
    var presenter = new NewsListPresenter(view, data, logging);
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    presenter.ReleaseView();
 
    // Assert
    Assert.IsNotNull(view.Model);
}

Our test plays the scenario when we get the data in the views load event and sets the views model.
We first arrange our view, data and logging, which are our parameters for our presenter. Next we tell the mock of the data layer, that when someone calls “GetNewsItems”, it should return a new instance of a generic list of NewsItems. We can then setup our presenter.

The presenter is created from the actual implementation, and passing the respective view, data and logging parameters for the constructor. This is the class we want to test!

Next we can act on the presenter, as if the view is instantiated and a request is made to the UserControl where the “Load”-method is called. When calling the load event, you have to pass 2 parameters; an “Object” and “EventArgs”, representing the sender and the arguments for the method. After the load event has fired, we can call the presenters “ReleaseView”-method, to remove our subscription from the views load method. This is normally done by the framework.

Last we assert, to check if everything is alright. In this case we only have to check that the views model is not null, or we would have made some mistake.

What happens if the data layer throws an exception when called? Let’s find out:

[TestMethod]
public void NewsListPresenterGetsNewsItemsFromModelTrowsException()
{
    // Arrange
    var view = MockRepository.GenerateStub<IView<List<NewsItem>>>();
    var data = MockRepository.GenerateMock<IData>();
    var logging = MockRepository.GenerateMock<ILogging>();
    var loggingMessage = "Could not get news items from data layer";
    var loggingException = new Exception();
 
    data.Stub((d) => d.GetNewsItems()).Throw(loggingException);
    logging.Expect((l) => l.Write(loggingMessage, loggingException));
 
    var presenter = new NewsListPresenter(view, data, logging);
 
    // Act
    view.Raise(v => v.Load += null, view, new EventArgs());
    presenter.ReleaseView();
 
    // Assert
    Assert.IsNotNull(view.Model);
    logging.VerifyAllExpectations();
}

Again we arrange our view, data and logging, but in this case we also arrange a logging message and an exception, which we expect to occur.

The data layer is changed, so that when we call the “GetNewsItems”-method, it throws an exception. When the data layer throws an exception, we expect that the logging framework is going to be called with the parameters of a string and the exception thrown by the data layer.

We act on the presenter, and raise the load event. We then release the view from the presenter in “ReleaseView”.

Finally we assert, and we still make sure that the views model is nut null and then we verify all the expectations on the logging mock. The “VerifyAllExpectations” is an extension method from the Rhino framework, which helps assert any expectations setup by our code. In this case we expect that the logging framework is called with a specific string and exception thrown by the data layer.

Results:

image002

Note
It’s important to note that the IData and ILogging implementations delivered to the presenter are assumed to be tested and valid. A separate project should implement and test these, where in this project it is not.

Finally – the UserControl

Create a new UserControl in a subfolder called “UserControls” and name it “NewsList” (In the web project). Here you get 3 files: NewsList.ascx, NewsList.ascx.cs and NewsList.ascx.designer.cs. Open up “NewsList.ascx.cs” and change the extended class to “MvpUserControl<List<NewsItem>>” and set a “PresenterBinding” attribute on top of the class with the argument “typeof(NewsListPresenter)” as its argument:

[PresenterBinding(typeof(NewsListPresenter))]
public partial class NewsList : MvpUserControl<List<NewsItem>>
{
}

This is your code behind file, which only needs other methods if any events or custom databing needs to occur.

Note
The “PresenterBinding” attribute is part of the WebformsMVP framework, but is supposed to disappear in a future version. The attribute tells the framework which presenter to use, and in future versions it will be discovered automatically via a naming convention as seen in ASP.NET MVC.

Using the class “MvpUserControl<T>” as the extended class compared to the normal “UserControl” from ASP.NET gives us access to the model delivered by the presenter. The generic argument of “MvpUserControl” is the type of the model shared. In our news list case, the model is a list of news items. We can access the model via the property “Model”, and use it in our markup – open the “NewsList.ascx” file:

<a href="AddNewsItem.aspx">Add new news item</a><br />
<asp:Repeater runat="server" DataSource="<%# this.Model %>">
    <HeaderTemplate>
        <table cellpadding="2" cellspacing="0" border="1">
    </HeaderTemplate>
    <ItemTemplate>
        <tr>
            <td>
                <h3>
                    <%
   1: # Server.HtmlEncode(((WebformsMvpDemo.Logic.Views.Model.NewsItem)Container.DataItem).Title) 
%></h3>
                <p class="newsDate">
                    <%
   1: # ((WebformsMvpDemo.Logic.Views.Model.NewsItem)Container.DataItem).Date.ToString("dd/MM-yyyy hh:mm")
%></p>
                <p>
                    <%
   1: # Server.HtmlEncode(((WebformsMvpDemo.Logic.Views.Model.NewsItem)Container.DataItem).Content).Replace("\n", "<br />")
%></p>
            </td>
            <td>
                <a href="EditNewsItem.aspx?id=<%# ((WebformsMvpDemo.Logic.Views.Model.NewsItem)Container.DataItem).Id.ToString("N") %>">
                    Edit</a> | <a href="DeleteNewsItem.aspx?id=<%# ((WebformsMvpDemo.Logic.Views.Model.NewsItem)Container.DataItem).Id.ToString("N") %>">
                        Delete</a>
            </td>
        </tr>
    </ItemTemplate>
    <FooterTemplate>
        </table>
    </FooterTemplate>
</asp:Repeater>

The markup contains a link to create a new news item and a Repeater that displays the news items. The Repeater has its datasouse set to “this.Model”, since “this.Model” is our list of news items. For each item in the model, a table row is created with two columns. The first column contains the content of the news item, with title, date and content, and the second contains links to edit and delete for the respective news item presented. What’s missing here is the strongly typed items in the repeater, so a cast has to be made from the “Container.DataItem” and then the properties can be accessed.

Conclusion

WebformsMVP delivers a way to separate and test logic from presentation of data. UserControls can layout and present data via styles and so forth, but the problem is usually the logic.

Next blog post will contain more information regarding testing of more advanced presenters.

Code will be available in a later blog post.

Links



About the author

Claus Nielsen

My name is Claus Nielsen and I am a .NET developer located in Århus, Denmark.

Tag cloud

Month List

Sign in