Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Doing away with an Assembly Dependency


:P
On this page:

I'm looking at some oldish code that sits inside of a web control library. This is my main web control library were I keep all of my custom controls that I typically use. It includes all the AJAX controls, a custom databinder, some generic status display controls, a number of stock control extensions, plus a host of web control related helper routines used in control development in general. It's a fairly focused library assembly that's fully self contained without outside dependencies.

The databinder control of the library can run fully self contained, but I also have an optional dependency on my business class library. Basically the automatic databinding validation that takes place can take validation errors from my business objects and automatically shove those validation errors into the databinder so that the databinder's validation code can display these errors - in addition to unbinding errors - in a consistent fashion. It works great, but doing this introduces a dependency into my control project that I always have to remember when I distribute the control assembly when it's not used in relation to the business library (which is common).

So the question is how to deal with this effectively. I have been futzing around with this a bit today and while I have a working solution I'm not really happy with it so I thought I throw this out for some discussion here in case I'm missing something obvious.

So the original code inside of the Web Control library does basically this:

#if USE_WWBUSINESS
        /// <summary>
        /// Takes a collection of ValidationErrors and assigns it to the
        /// matching controls. These controls must match in signature as follows:
        /// Must have the same name as the field and a 3 letter prefix. For example,
        /// 
        /// txtCompany - matches company field
        /// cmbCountry - matches the Country field
        /// </summary>
        /// <returns></returns>
        public void AddValidationErrorsToBindingErrors(Westwind.BusinessObjects.ValidationErrorCollection errors)
        {
            foreach (Westwind.BusinessObjects.ValidationError error in errors)
            {
                Control ctl = wwWebUtils.FindControlRecursive(this.Page.Form, error.ControlID);
                this.AddBindingError(error.Message, ctl);
            }
        }
#endif

and I then have a project level #define constant that I have to set or unset for compilation.  The Westwind.BusinessObjects.ValidationErrorCollection is located in a separate generic business library assembly and so in order to compile the code I need to ensure that the assembly is available.

This works, but it's a hassle for two reasons:

  • It's easy to forget the switch and get compile errors
  • The assembly is used in several projects some of which require the switch others that don't
  • There's no way to tell looking at the assembly whether it has the switch set or not

Regardless though - once compiled - I can deploy the assembly even if the Westwind.BusinessObjects assembly is not present and the assembly works fine as long as the above method is not called.

So what if I want to abstract away the dependency?

The first and probably easiest alternative to do this is to do away with the concrete type altogether and then rely on Reflection to get the values. Something like the following works to remove the dependency:

/// <summary>
/// Takes a collection of ValidationErrors and assigns it to the
/// matching controls. These controls must match in signature as follows:
/// Must have the same name as the field and a 3 letter prefix. For example,
/// 
/// txtCompany - matches company field
/// cmbCountry - matches the Country field
/// 
/// The input parameter is a generic IList value, but the type should be
/// specifically Westwind.BusinessObjects.ValidationErrorCollection. The
/// generic parameter is used here to avoid an assembly dependence.
/// </summary>
/// <returns></returns>
/// </summary>
/// <param name="errors">Westwind.BusinessObjects.ValidationErrorCollection list</param>
public void AddValidationErrorsToBindingErrors(IList errors)
{            
    if (errors == null)
        return;
 
    foreach (object error in errors)
    {
        string controlID = wwUtils.GetProperty(error, "ControlID") as string;
        string message = wwUtils.GetProperty(error, "Message") as string;
 
        Control ctl = wwWebUtils.FindControlRecursive(this.Page.Form, controlID);
        this.AddBindingError(message, ctl);
    }
}

wwUtils.GetProperty() is a wrapper Reflection method that easily retrieves a property using Reflection and there are a bunch of routines on wwUtils that provide similar functionality to use Reflection to call methods, set values and provided 'pathed' Reflection access using . syntax (ie. wwUtils.GetProperty(loObject,"Address.Street")).

Again, this is functional and better since it removes the external dependency but it still doesn't make me happy because the method interface is not very clean to the end user - they have no idea what's expected as an input value at this point other than that it's an IList interface. Yet it won't really work unless the specific ValidationErrorCollection is passed. Hence the rather lengthy comment and explicit type description in the parameter comment which hopefully shows up with the XML documentation file if shipped.

Unfortunately I can't really think of a cleaner way to do this and preserve the type interface as anything that provides the type - Westwind.BusinessObject.ValidationErrorCollection - would introduce the assembly dependence. Using an interface to describe the errors is no better because there too the type would need to be present in both assemblies somehow. I can't see a way to share the type or interface without some sort of external reference.

The only other thing I could possibly see would be to subclass the control and pull it back into the business project or more appropriately into a Web project. The control could then get a new name and provide an override with the explicit type. But that too would suck because it'd have to be done for every web project.  Or pull the control subclass into the business project which would work, but be pretty unclean and then require a System.Web in the business library which also is not very clean.

So what do you think? Is there another way to approach this without polluting one or the other assemblies with assembly references that shouldn't be there?

Posted in ASP.NET  CSharp  

The Voices of Reason


 

Kirk Clawson
January 12, 2008

# re: Doing away with an Assembly Dependency

Are you specifically trying to avoid generics? If not, then what about IDictionary<TKey, TValue> or IList<KeyValuePair<TKey, TValue>>?

It still wouldn't eliminate the need for documentation specifying that the key is the controlId and the value is the message, but it gets you closer. The only way I can see to avoid that is a base library with either an interface or a ControlId/Message struct that can be used in an IList

Rick Strahl
January 12, 2008

# re: Doing away with an Assembly Dependency

No Generics would be fine, but I'm not sure that that would help. As you say there still would be the documentation issue.

The list type that is passed in actually pre-exists on another object and that list is strongly typed. So, creating the key/value pairs there is handled on the business object (ie. AddValidationError() or use an explict new ValidationError(Message,ControlID)). It's the inbound interface for the data binder that has the generic IList parameter that really should be something like List<ValidationError> or ValidationErrorCollection but which is defined in the other assembly.

I thought about interface abstraction but it comes back to the same thing - the type needs to exist in both assemblies for it to be useful and that means one or the other needs a reference to the other.

I suspect from a language perspective there's no alternative to what I have here, but maybe some sort of other pattern to represent this data would work a little cleaner might apply and I'm not seeing it.

Hernan Garcia
January 13, 2008

# re: Doing away with an Assembly Dependency

I don't see a point to avoid the reference to an external library that contains the ValidationErrorCollection. I think that problem is the ValidationErrorCollection doesn't belong to either the BusinnesObject nor the WebControls but to a third library that both reference all the time.
You may decide to put an Interface into this third library instead of the object, that depends on your architecture.
I don't know the code in ValidationErrorCollection, so not sure how this is implemented but this is the solution I usually implement to avoid this type of dependencies issues.
This way you can compile both libraries without changes.
May this help?

Siderite
January 13, 2008

# re: Doing away with an Assembly Dependency

I may not have understood what the problem is, but I think you need something like an 'implements' keyword or function that takes and object and verifies it implements an interface without being declared as one. Then you can assert that the objects in the IList respect an interface that you can document. Does it make sense? This way you don't have to retrofit all the other libraries to have objects that implement it. Although, that would be the best way in my book.

Marco von Frieling
January 13, 2008

# re: Doing away with an Assembly Dependency

Hi Rick,

please have a look at the sources of Rob Cnoery's SubSonic (www.subsonicproject.com). I'v been struggled with type not found exceptions for MySql.Data an in another project for the enterprise library (containg MS SQL data access code) until I found out that the required assemblies are neither in the bin folder nor in the GAC, but all of them referenced in the SubSonic assembly. For me it seems that .NET does nothing if it fails to load an assembly, but throws an exception if in should use a type which is not loaded. Maybe you have a look at the SubSonic sources and try to use a (runtime) configuration option which enables/disables your business object validations and only call methods using types from you bo assembly if that option is enabled.

BTW: Some time ago I found some problems in your AppConfiguration class, esp. concerning System.Timespan. Please give me a note to which email address I can send you my changes in that class.

Kind regards,

Marco

Gates VP
January 13, 2008

# re: Doing away with an Assembly Dependency

Hey Rick;
I think that you have a classic inter-library (or class) communications problem here.

Conditional compilation, as you've done, is one solution, but generally not the preferred method (as you've pointed out). Of course, your solution exactly mirrors what you're trying to do, you want that class to have functionality contingent on the existence of another class reference.

So you implemented the solution you needed, but as you pointed out, it didn't feel right. My guess is that you've actually solved the wrong problem. You're implementing kind of a top-down library hierarchy, but then you're pulling types from a lower level.

Architecturally, there are two solutions here:
1. Move the base definition of the type you're "pulling".
2. Standardize the communication interface.

You can't do #1, b/c the "other" library is stand-alone. So you have to do #2. However, you want both libraries to be independent.

So either you use an existing type to which both libraries already have access: Exception? Pass in strings b/c that's all that you're using? Truth is, the whole "GetProperty" method is just a method of standardizing the interface, so it's probably as good as any.

Otherwise, this sounds like a textbook case of anonymous types (LINQ?) and Extension methods.

Marcin Kasperski
January 14, 2008

# re: Doing away with an Assembly Dependency

Hi Rick,

I think you want to achieve two things here: 1) decouple the libraries, and 2) preserve the meaning of the methods and their parameters (to avoid the documentation problem).

The first requirement you already covered by having IList parameter (or IDictionary<string, string>), but that doesn't cover the second requirement.
The original implementation, on the other hand, covers the second requirement, but not the first one. I believe you can have both by employing an adapter-like pattern here.

The way I'd suggest to solve this is:

1. Create a ValidationErrorsCollection (name it as you like) class that resides in your controls library and is specific to the functionality of collecting error messages for the form. It has nothing to do with your business class (covers the first requirement). The new class would be used in the AddValidationErrorsToBindingErrors method. It would essentially have a IDictionary<string, string>-like structure underneath, but would also convey the meaning of what it holds (covers the second requirement).

2. Create a static adapter method on the new Controls.ValidationErrorsCollection class to accept some commonly accessible DTO-like object, which in your case is IDictionary<string, string>. That means create a Controls.ValidationErrorsCollection.FromDictionary(IDictionary<string, string). Set the name this method to whatever satisfies you in terms of the second requirement.

3. In your Business.ValidationErrorsCollection class create an export method, that will push the elements into the generic-purpose DTO object (the IDictionary<string, string>): Business.ValidationErrorsCollection.ToDictionary()

4. In your codebehind/controller class bridge the two collections together like:
page.AddValidationErrorsToBindingErrors(
    Controls.ValidationErrorsCollection.FromDictionary(Business.ValidationErrorsCollection.ToDictionary())
);


That's a basic quick-n-dirty implementation. You can further play with it to make it more elegant, e.g:
* move the .FromDictionary functionality to a constructor overload;
* have the .ToDictionary be implemented as a implicit cast operator;
* rename the Controls.ValidationErrorsCollection to something different than the Business class (lets say BindingErrorsCollection);
* rename the AddValidationErrorsToBindingErrors to something shorter (like AddBindingErrors);
* optionally move all this into Extension methods and use it on a per-case basis.

Having all this done you would end up with:
page.AddBindingErrors(new BindingErrorsCollection(Business.ValidationErrorsCollection));


The binding errors now can come from any place (not just your business layer), as long as the source can be converted to the DTO object (the dictionary). That opens a few more possibilities.

Hope this solves your problem,

Marcin.

Rick Strahl
January 14, 2008

# re: Doing away with an Assembly Dependency

@Gates - yup you got the dilemma exactly.

Extension methods aren't an option in this scenario because the libraries need to work in plain 2.0, but even with them I still wouldn't solve the clear interface issue. Anonymous types would have to know exactly what to pass ({ { Message="", ControlID="" },{...} }) as well which requires more knowledge than passing the type. But I just realized that even as it is now the code works with any List type that has a member object that includes Message and ControlID properties so the method would actually work with anonymous types which is actually a useful use case.

@Marcin - yeah that would work and is kind of along the way I was thinking but didn't quite figure out the finer details. But in the end this too requires a non-strongly typed value (the list or dictionary) so while it is more explicit it still ends up being 'generic'.

As I mentioned above I think even with the IList parameter the input can come from anywhere as long as Message and ControlID properties are present (actually only Message since the wwUtils.GetProperty calls would result in null if they fail and just the message would add).

But this totally gets me thinking about another way to look at this and how any errors can be added to the binding errors collection. In fact, I'm going to add a more 'generic' overload for the AddBindingError() method that adds a single bindingError.

Thanks all for the discussion - sometimes it's just extremely useful to see how others look at the same problem when you work in - gulp - isolation. <g>

biscuit
January 24, 2008

# re: Doing away with an Assembly Dependency

Hi Rick,

At present the library of the caller of the wwDatabinder.AddValidationErrorsToBindingErrors() (let's call it LibraryA) must already have a reference to both the wwDataBinder and business object library in order to make the call?
(as it will be making a call to MsdnMag.Web.Controls.wwDatabinder.AddValidationErrorsToBindingErrors(errors) where errors is a Westwind.BusinessObjects.ValidationErrorCollection type).

If so, I would say your solution might be to introduce new DataBinderValidationErrorCollection and DataBinderValidationError classes into the project and have a utility method in LibraryA that knows how to convert a BusinessObjects.ValidationError into a DataBinderValidationError (and its collection).

This is duplicating the error container classes but does introduce the strong typing you need - is there a more complicated reason why you can't do this?
e.g. the caller in LibraryA is more generic and does not call wwDatabinder.AddValidationErrorsToBindingErrors() directly?

Rick Strahl
January 24, 2008

# re: Doing away with an Assembly Dependency

@Biscuit - the more 'complicated' reason is that this is a library component that's meant to be generic. Introducing special types into a project is not an option for the consumer, but would make sense under other circumstances. I believe that this would be the ideal solution in the end that is also 'politically correct', it's just in this case it's not appropriate.

In the end this is a specialty case, where a generic library adds a non-generic feature <s>. In reality having the dependency on the business library actually works in production - it's at compile time that the compiler complains about the missing assembly reference.

SxC
July 24, 2008

# re: Doing away with an Assembly Dependency

Thanks for your kind sharing ,
Where can I get the latest source code of wwDatabind?

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024