ObjectBuilder Customization For User Controls & Master Pages

Topics: Web Client Software Factory
May 12, 2007 at 10:55 AM
Edited May 12, 2007 at 11:19 AM
Hello All,
I have been using Microsoft Web Client Factory for quite a while now. The major problem that i faced was that ObjectBuilder didn't work on User Controls and master pages. I went through the codeplex forums and got some solutions and tried it. But in my scenario i had to add user controls at runtime for which the proposed solutions didn't work. So me and my colleage Ahmed Saeed finally decided to dive into the customization of the client factory. We customized the factory particularly working on the Microsoft.Practices.CompositeWeb.WebClientApplication. We are now using our custom web client factory and it works perfectly for user controls both added at design time and runtime. Here is the code...
        protected void InnerPreRequestHandlerExecute(IHttpContext context)
		{
			if (HttpRequestHelper.IsHandledByPageHandlerFactory(context.Request.Url.ToString()))
			{
				ICompositionContainer moduleContainer = GetModuleContainer(context);
 
				CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, context.Handler);
                                                                Page page = context.Handler as Page;
 
				if (context.Handler is Page)
				{
                                                                                page.PreInit += new EventHandler(OnPagePreInit);
					PrePageExecute(context.Handler as Page);
				}
			}
		}
 
        private void OnPagePreInit(object sender, EventArgs e)
        {
            Page page = ((Page)(sender));
            page.PreInit -= new EventHandler(OnPagePreInit);
            ICompositionContainer moduleContainer = GetModuleContainer(new Microsoft.Practices.CompositeWeb.Web.HttpContext(System.Web.HttpContext.Current));
            if (!(page.Master == null))
            {
                CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, page.Master);
            }
            BuildControls(moduleContainer, page.Controls);
        }
 
 
        private void BuildControls(ICompositionContainer moduleContainer, System.Web.UI.ControlCollection controls)
        {
            foreach (Control currentControl in controls)
            {
                if (currentControl is UserControl)
                {
                    CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, currentControl);
                }
                BuildControls(moduleContainer, currentControl.Controls);
            }
        }
 
 
		protected virtual void PostPageExecute(Page page)
		{
            ICompositionContainer moduleContainer = GetModuleContainer(new Microsoft.Practices.CompositeWeb.Web.HttpContext(System.Web.HttpContext.Current));
            TearDownControls(moduleContainer, page.Controls);
		}
 
        private void TearDownControls(ICompositionContainer moduleContainer, System.Web.UI.ControlCollection controls)
        {
 
            foreach (Control currentControl in controls)
            {
                if (currentControl.GetType().Equals(Type.GetType("UserControl")))
                {
                    this.PageBuilder.TearDown<System.Web.IHttpHandler>(moduleContainer.Locator, Context.Handler);
                }
                TearDownControls(moduleContainer, currentControl.Controls);
            }
 
        }

help taken from the following thread...

http://www.codeplex.com/websf/Thread/View.aspx?ThreadId=8496

May 12, 2007 at 11:09 AM
hi everyone,
i would really like the codeplex guys to comment on it and highlight any problems that you may find in the code. We have a composite web application that has about 500 Web Pages and covers most of the scenarios. We have tried every scenario and its working fine. We have to release this to other development teams working on the application. So any input from guys over here is highly precious for us.

Regards,

Ahmed Saeed.
May 15, 2007 at 9:36 AM
You guys did very well.
May 15, 2007 at 3:51 PM
Hi,
thanks HenryJao , I hope other guys are also looking at this thread. I am anxiously waiting for a response from you guys that gives a critical feedback. Because as i earlier said we have to release it to other development teams. So I want you guys to confirm this solution. I would also like to know that whether the next release gives a "Add User Control With Presenter" Automation Recipe or not. Because we are also trying to build this automation recipe in our customization of the Web Client Factory and whether we can build this in our customization, I hope we are not wasting our time with it. So Kindly give your input.

Regards,

Ahmed Saeed.
May 15, 2007 at 4:29 PM
Here are my thoughts

1) You can probably do this with no customization to the Composite Web Application Block. The code below just creates a custom web application class and overrrides the pre- and post- page execute methods.

2) Are you sure you don't have to wait until Page Initialization Completes? I don't know for sure, but I think attaching to the InitComplete Event instead of Init would be wise to be sure that all user controls have been created and master page file is set, etc.

3) Why do you do if (currentControl is UserControl) in buildup but currentControl.GetType().Equals(Type.GetType("UserControl") in teardown? My recommendation is to stay consistent and use if (currentControl is UserControl) in both places.

Here is another look at this code, but I admit I haven't run it :)

public class CustomWebClientApplication : WebClientApplication
{
    protected override void PrePageExecute(Page page)
    {
        base.PrePageExecute(page);
        page.InitComplete +=new EventHandler(OnPageInitComplete);
    }
 
    private void OnPageInitComplete(object sender, EventArgs e)
    {
        Page page = ((Page)(sender));
        page.InitComplete -= new EventHandler(OnPageInitComplete);
        ICompositionContainer moduleContainer = GetModuleContainer(new HttpContext(System.Web.HttpContext.Current));
        if (!(page.Master == null))
        {
            CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, page.Master);
        }
        BuildControls(moduleContainer, page.Controls);
    }
 
    private void BuildControls(ICompositionContainer moduleContainer, ControlCollection controls)
    {
        foreach (Control currentControl in controls)
        {
            if (currentControl is UserControl)
            {
                CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, currentControl);
            }
            BuildControls(moduleContainer, currentControl.Controls);
        }
    }
 
    protected override void PostPageExecute(Page page)
    {
        base.PostPageExecute(page);
        ICompositionContainer moduleContainer = GetModuleContainer(new HttpContext(System.Web.HttpContext.Current));
        TearDownControls(moduleContainer, page.Controls);
    }
 
    private void TearDownControls(ICompositionContainer moduleContainer, ControlCollection controls)
    {
        foreach (Control currentControl in controls)
        {
            if (currentControl is UserControl)
            {
                this.PageBuilder.TearDown<IHttpHandler>(moduleContainer.Locator, Context.Handler);
            }
            TearDownControls(moduleContainer, currentControl.Controls);
        }
    }
}

Let me know if it works if you try it. I am certainly more than happy to help you with this. I will take a better look at the code tonight when I have more time.

Since this doesn't require a custom change to the Composite Web Application Block, this could be contributed to the Web Client Software Factory Contribution Project when it is made available.

Regards,

Dave

________________________________

David Hayden
Microsoft MVP C#
May 16, 2007 at 3:41 AM
Edited May 16, 2007 at 4:46 AM
I tried it tonight as promised.

A few things.

1) The MasterPage is a UserControl so this bit of code is redundant and un-necessary since we will pick it up in the loop for UserControls. Removed-

    if (!(page.Master == null))
    {
        CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, page.Master);
    }

2) This code is a little embarassing and shouldn't even be in here. All it does is loop and call the same thing over and over and over and over. It is not tearing down individual controls but tearing down the page multiple times. The page will be removed in the InnerPostRequestHandlerExecute Method for us, so we don't need to keep doing it here. Removed-

    protected override void PostPageExecute(Page page)
    {
        base.PostPageExecute(page);
        ICompositionContainer moduleContainer = GetModuleContainer(new HttpContext(System.Web.HttpContext.Current));
        TearDownControls(moduleContainer, page.Controls);
    }
 
    private void TearDownControls(ICompositionContainer moduleContainer, ControlCollection controls)
    {
        foreach (Control currentControl in controls)
        {
            if (currentControl is UserControl)
            {
                this.PageBuilder.TearDown<IHttpHandler>(moduleContainer.Locator, Context.Handler);
            }
            TearDownControls(moduleContainer, currentControl.Controls);
        }
    }

3. Recursively calling through all controls is ugly. I think it might be more realistic to just recurively call through UserControls. I added the BuildControls Call within the If Statement. Personally, I don't know why anyone would add a usercontrol to a usercontrol so I think the recursive call may be unnecessary. However, since the MasterPage is a UserControl, chances are it will have other UserControls so we need to add it. This shows moving the BuildControls(moduleContainer, currentControl.Controls); up within the if statement.

    // ...
 
    if (currentControl is UserControl)
    {
        CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, currentControl);
        BuildControls(moduleContainer, currentControl.Controls);				
    }
 
    // ...


So, given that, this is the best I can come up with at the moment:

public class CustomWebClientApplication : WebClientApplication
{
    protected override void PrePageExecute(Page page)
    {
        base.PrePageExecute(page);
        page.InitComplete +=new EventHandler(OnPageInitComplete);
    }
 
    private void OnPageInitComplete(object sender, EventArgs e)
    {
        Page page = (Page)sender;
        page.InitComplete -= new EventHandler(OnPageInitComplete);
        ICompositionContainer moduleContainer = GetModuleContainer(new HttpContext(System.Web.HttpContext.Current));
        BuildControls(moduleContainer, page.Controls);
    }
 
    private void BuildControls(ICompositionContainer moduleContainer, ControlCollection controls)
    {
        foreach (Control currentControl in controls)
        {
            if (currentControl is UserControl)
            {
                CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, currentControl);
                BuildControls(moduleContainer, currentControl.Controls);				
            }
        }
    }
}

My hope is that the single call:

    this.PageBuilder.TearDown<IHttpHandler>(moduleContainer.Locator, context.Handler);

in InnerPostRequestHandlerExecute will properly tear down everything built-up in the above code. I don't have time to look at it tonight, but maybe someone from the WCSF Team can comment.

Regards,

Dave

___________________________________

David Hayden
Microsoft MVP C#
Jun 27, 2007 at 1:55 PM
Hi,

First of all I am sorry for replying so late. As we are busy in the final release of our project. Now I want to thank Mr. David Hayden for his valuable time and very constructive input which helped us a lot.
Although the Solution proposed by David is not working in our scenarios which is dynamically adding user controls.
So we use the InnerPreRequestHandlerExecute Event which helped us. Although we remove the Teardown controls related code.
this.PageBuilder.TearDown<IHttpHandler>(moduleContainer.Locator, context.Handler);

We put the BuildControls function also outside if because to check if the usercontrol is inside some container controls like in our case it is TabControl.
_
if (currentControl is UserControl)
{
CompositionContainer.BuildItem(PageBuilder, moduleContainer.Locator, currentControl);
BuildControls(moduleContainer, currentControl.Controls);
}
_

Regards,


Zeeshan Waheed
Jun 29, 2007 at 6:09 PM
I'm a newby of the Web Client Software Factory.

I noticed only in aspx files the following method is called (and not in asmx pages)

CreateNew
public View1Presenter Presenter
{
set
{
this._presenter = value;
this._presenter.View = this;
}
}


As cleary explayned in thread http://www.codeplex.com/websf/Thread/View.aspx?ThreadId=10847 , "currently the CWAB is not building User Controls with ObjectBuilder, hence you don’t have Dependency Injection over them".

I see in thread http://www.codeplex.com/smartclient/Thread/View.aspx?ThreadId=11345 the following suggestion (not completely clear to me: how can I make this contructor called?) :

"A simple workaround could be to inject the presenter in the View’s constructor, something like:

public View(CreateNew ViewPresenter newPresenter)
{
_presenter = newPresenter;
InitializeComponent();
this.Font = new System.Drawing.Font("Tahoma", 8F);
}

This way the _presenter will be assigned before the control firing the onLoad event"


In your threath (http://www.codeplex.com/websf/Thread/View.aspx?ThreadId=10181) it seams that a solution can be found extend Web Client Software Factory (but it is not clear to me how to procede to do that changes).

Could some one cleary explain to a newby of the Factory, what is the "easy way" to make the ObjectBuilder's Dependency Injection work on ascx files? I'm a bit confused so please make an example so that everybody can take advantage.
I think that the use of ascx files is a "must", so the Web Client Software Factory should provide an easy way to support their usage.

Thanks

Enzo


Jul 6, 2007 at 6:55 AM
Hi Enzo,

I think You have to look at the following Project.
http://www.codeplex.com/wcsfcontrib
I hope this will help you find out how to customize Web Client Software Factory.
If after going through that you still got problems then let me know I will try to explain you.

Regards,
Zeeshan Waheed
Jul 17, 2007 at 5:04 AM
@Zeeshan

We have added support for dependency injection on Master Pages, User Controls and ASMX in the next release. Check out the latest community drop.

Glenn
Jul 17, 2007 at 7:16 AM
Hi Glenn,

Great news . Thanx for the update I am really looking forward for this change.

Zeeshan
Jul 17, 2007 at 9:36 AM
Great!!
Are you talking about the "WCSF vNext Weekly Drop July 13, 2007 " or, as I suppose, the "next release" is something that will be submitted in a few days (possibly tell us when ...)?


Thank you to let us be informed in real time!!

Enzo
Jul 17, 2007 at 9:42 AM
@continienzo

I am talking about the July 13th drop. The next official release will not be until around September. The current community drops contain RIs and some CWAB enhancements (DI) and AJAX extensions.

Glenn
Jul 17, 2007 at 3:40 PM
Hi Glenn.

I downloaded the source file using the command

cpc checkout websf

so I suppose I've dowloaded the last drop of the project (July 13, 2007 ??? How can I verify it?). Then I compiled the release version for oall project (included, in particolar, ObjectBuilder.WCSFExtensions where I suppose the enhancement to suppost User Control should be included). Then I modify again the global.asax content to

<%@ Application Language="C#" Inherits="Microsoft.Practices.CompositeWeb.WebClientApplication" %>

instead of (following the example found in http://www.codeplex.com/wcsfcontrib)

<%@ Application Language="C#" Inherits="WCSFContrib.Samples.CustomWebClientApplications.UserControlsMasterPages.CustomWebClientApplication" %>

.... and I have (in the Page_Load of the ascx) the same nullReferenceException error I had before using the wcfscontrib dll (since in our ascx file we need the object builder to create our presenter - through the CreateNew directive )

So it seams that the release I've got still have problems with Master Pages + User Controls !!!

Any suggestion? Did I make some mistake in updatinf the dll of the factory?

Thank you
Enzo
Jul 18, 2007 at 6:48 AM
Edited Jul 18, 2007 at 6:51 AM
@continienzo

U should read the readme.txt file that came with the source code.

The major change this week is to how CWAB does dependency injection. Previously, dependency injection was performed on ASPX pages automatically as they were created. Unfortunately,
 that approach doesn't work for master pages, controls, or web services. In each of those cases, there is no consistent hook to intercept construction of these objects. In order to enable dependency injection, 
we've changed how the DI container is accessed.
 
Now, instead of happening automatically, an object that wishes to be injected must call a single static method:
 
    			Microsoft.Practices.CompositeWeb.WebClientApplication.BuildItemWithCurrentContext(this);
 
This will cause the DI container to walk the given instance, and resolve any dependencies. The disadvantage is that this isn't really dependency injection anymore;
 the object must know that there is a DI container and invoke it. However, it has the major advantage that getting dependencies resolved now works in exactly the same manner across pages,
 master pages, controls, and web services. Among other things, this enables building user controls and master pages using the MVP pattern.

Regards
Benny
Jul 18, 2007 at 9:47 AM
Thanks Benny ... I didn't notice the readme file.

But now reading youir words ("an object that wishes to be injected must call a single static method ....This will cause the DI container to walk the given instance, and resolve any dependencies. The disadvantage is that this isn't really dependency injection anymore; the object must know that there is a DI container and invoke it") I wonder if it wasn't better the solution suggested by http://www.codeplex.com/wcsfcontrib (using the WCSFContrib.CompositeWeb.dll) where the only change to be done, to make User control works, is in the global.asax:

<%@ Application Language="C#" Inherits="WCSFContrib.Samples.CustomWebClientApplications.UserControlsMasterPages.CustomWebClientApplication" %>

Can you explain why the solution taken in the July 13 should be better and I should use it instead of continuing to use the suggestion found in wcsfcontrib?

Thanks again
Enzo
Jul 24, 2007 at 5:09 PM
Hi

I just thought I would post this solution since I couldn't find any info on it previously. When I used the above solution all of my code that used a .FindControl method stopped working.

I am unsure why but when I built the user controls it did something.

Anyway to solve the problem I changed the event to LoadComplete instead of PagePreInit.

I also posted a quick tutorial on it at http://www.dotnetcore.com/tutorial.aspx?id=10

Thanks for the code ZeeshanWaheed, it was a big help.

Adam
Jul 25, 2007 at 6:57 AM
Edited Jul 25, 2007 at 6:59 AM
@Continenzo

The team evaluated each of the different possible approaches and felt that although this introduced the need for the object to build itself up, it was safer and provided a more consistent approach for building up each of the different types. Master Pages and User Controls have a different lifecycle than normal Pages. As such there's no magic point within User Controls or Master Pages where we can safely assume that all the controls exist and can be built up.

We did find that (as some folks pointed out in the discussion lists) an undocumented side effect of accessing the Master Page property at the right point seems to force all the controls to be created. However, we felt uncomfortable relying on an undocumented side effect of a call to a property getter. We did try to inject at just about every point in the Page life cycle, looking for a solution that would work for all cases (Pages, User Controls, and Master Pages), and failed to find one. The next-best alternative is the approach we have started using in the vNext weekly code drops, which as mentioned has the added benefit of working for ASMX web services (and possibly WCF Services in the future) as well.

Regards
Glenn
Sep 22, 2007 at 4:57 PM
I have been reviewing the latest source control quickstarts. I'm assuming the quickstarts haven't been updated to reflect an item needing to build itself up, yet there are still UserControls that use dependency injection, or am I just blind (wouldn't be the first time)?
Sep 22, 2007 at 10:33 PM
@katokay

Based on feedback we created seperate base classes that now encapsulates doing the injection for Pages, Master Pages and User Controls. Our quickstarts in the drops have most likely been updated to now utilize the base class. The upside of this is it means you don't have to add the manual lines of code throughout your app.

Hope this helps
Sep 22, 2007 at 11:14 PM
Thanks for the reply. I now see the base class differences in the Quickstarts. On a side note, I like the additional pattern demonstrations.
Dec 11, 2007 at 11:16 AM
Edited Dec 11, 2007 at 11:19 AM
Thanks!!
I downloaded the last version of WCSF and used the new base classes (as seen in the MVPWithCWABQuickStart) and I solved some problems I had in the visualization of the top menu when User Controls were used.

Kind Regards
Enzo