views:

59

answers:

3

UPDATED: Added some sample code to help clarify.

Hi, Feel like this shouldn't be that complicated, but I think I just don't know the proper name for what I'm trying to do. I'm dealing with an ASP.net project.

The concept is pretty simple: I have a library that supplies some ecomm functions. One class in the libary contains functions around calculating tax. One class in the library revolves around the cart header. This class is consumed by a web project.

There will be a method like: carthead.SaveCart. Inside SaveCart, it will need to call tax.CalculateTax

What I need to do is to figure out a way to allow the carthead.SavCart to always have access to a specific function such as tax.CalculateTax (e.g. that function must always be available to the library).

However, I then want to allow anyone to create a different version of the tax.CalculateTax method.

I've tried doing some stuff with an overridable base class, but what I find is that even though override the tax base class in the web project, it only calls the overridden version of the tax class when I make a call from the web project. I can't make the tax class into an interface as when I do that, I cannot define the results of tax.CalculateTax as a list of (because in this instance t is an interface, not an actual class) - this list has to be consumed by the carthead.SaveCart method.

Therefore, when you step through the code, what you find is that when the web project calls the carthead.SaveCart method, the carthead.SaveCart method doesn't have access back to the overridden code from the web project... and results in calling the non-overridden version of the tax.CalculateTax method.

I'm sure I'm missing something, but I'm not even sure what it is I should be researching or what exactly the proper name is for what I'm trying to accomplish.

Essentialy to boil it down, I need to be able to override a method and have the overridden version of the method called from the non overridable methods in the library.

Can anyone point out to me what I've messed up or what I should be looking at?

UPDATE: Added some code snippets to help clarify:

Here is a piece of the CartHeader class in question:

Public Class CartHeader

'other class stuff omitted

Public Function UpdateCartStep2(ByVal CartNo As Long, ByVal Username As String, _
ByVal PmtMethod As String, ByVal ShipMethod As String, _
ByVal ShipComplete As Boolean, ByVal ShipCost As Double, _
ByVal ShipInstr As String, Optional ByVal TaxGroup As String = "", _
Optional ByVal PickupLoc As String = "", _
Optional ByVal FuelSurcharge As Double = 0, _
Optional ByVal Misc As String = "", _
    Optional ByVal TaxThisSomeTaxOrder As Boolean = False, _
    Optional ByVal ShipToID As Long = 0, _
    Optional ByVal ShipToZip As String = "", _
    Optional ByVal mCustCode As String = "", _
    Optional ByVal CustTax As Tax = Nothing) As Integer
    '=================>
    'note that the last parameter is new which is what we're currently using to pass in the customtax class so that we can consume it inside this method
    '==================>


    If IsNothing(CustTax) Then
        CustTax = New Tax
    End If

    '6-29-08 this stored proc was updated to allow for fuel surcharge
    'added fuel surcharge parameter

    Dim Resultval As Integer
    Dim strConnect As New SqlConnection(ConfigurationManager.ConnectionStrings("ConnectionString").ConnectionString)
    Dim SqlCommand As New SqlCommand("sp_UpdateCartStep2", strConnect)

    Try

        Dim SubTotalAmt As Double
        SubTotalAmt = GetCartSubTotal(CartNo)

        GetCartHeader(CartNo)

        Dim CartTax As Double

        Dim SystemTypeID As Integer = CInt(ConfigurationManager.AppSettings("SystemTypeID").ToString)

        Select Case SystemTypeID
            Case 1

                If profile.AllowTerms = False Then
                    CartTax = CalcTax(SubTotalAmt, ShipCost, FuelSurcharge, m_Ship_State_Province)
                Else
                    CartTax = 0
                End If
            Case 2
                '6-29-08 added to figure fuel surcharge
                'Dim CustTax As New Tax
                'Dim CustCode As String = System.Web.HttpContext.Current.Profile("CustCode")
                Dim lCustTax As New List(Of Tax)

                '=========================>
                'note that this part of the header must always call into the calctax method.
                'it should be able to call the custom method if it has been defined.
                lCustTax = CustTax.wa_cc_CalcTax(mCustCode, ShipToID, SubTotalAmt, ShipCost, FuelSurcharge, CStr(m_Ship_State_Province), CStr(ShipToZip))
                '==========================>


                For Each ct As Tax In lCustTax
                    CartTax += ct.Tax
                Next
                'CartTax = CalcTax(SubTotalAmt, ShipCost, FuelSurcharge, m_Ship_State_Province, TaxGroup)

        End Select

        SqlCommand.CommandType = CommandType.StoredProcedure
        strConnect.Open()

        SqlCommand.Parameters.AddWithValue("@CartNo", SqlDbType.BigInt).Value = CartNo
        SqlCommand.Parameters.AddWithValue("@Username", SqlDbType.VarChar).Value = Username
        SqlCommand.Parameters.AddWithValue("@PmtMethod", SqlDbType.VarChar).Value = PmtMethod
        SqlCommand.Parameters.AddWithValue("@ShipMethod", SqlDbType.VarChar).Value = ShipMethod
        SqlCommand.Parameters.AddWithValue("@ShipCompleteFlag", SqlDbType.Bit).Value = ShipComplete
        SqlCommand.Parameters.AddWithValue("@ShipCost", SqlDbType.Money).Value = ShipCost
        SqlCommand.Parameters.AddWithValue("@Tax", SqlDbType.Money).Value = CartTax
        SqlCommand.Parameters.AddWithValue("@ShipInstr", SqlDbType.VarChar).Value = ShipInstr
        SqlCommand.Parameters.AddWithValue("@PickupLoc", SqlDbType.VarChar).Value = PickupLoc
        SqlCommand.Parameters.AddWithValue("@FuelSurcharge", SqlDbType.Float).Value = FuelSurcharge

        '1-30-08 Changed to accomodate holding the carrier number when selecting collect freight
        'required modification of the sp_UpdateCartStep2 stored procedure.
        SqlCommand.Parameters.AddWithValue("@Misc3", SqlDbType.VarChar).Value = Misc3

        SqlCommand.ExecuteNonQuery()
        Resultval = 0
    Catch ex As Exception
        Resultval = -1
        System.Web.HttpContext.Current.Trace.Write(ex.Message)
    Finally
        strConnect.Close()
    End Try

    Return Resultval
End Function

End Class


This is the class we use as a base class... it have the wa_cc_calctax overridden if the base function calcs don't apply.

Public Class Tax


Private _Tax As Double
Public Property Tax() As Double
    Get
        Return _Tax
    End Get
    Set(ByVal value As Double)
        _Tax = value
    End Set
End Property

Private _TaxRate As Double
Public Property TaxRate() As Double
    Get
        Return _TaxRate
    End Get
    Set(ByVal value As Double)
        _TaxRate = value
    End Set
End Property


Private _TaxDesc As String
Public Property TaxDesc() As String
    Get
        Return _TaxDesc
    End Get
    Set(ByVal value As String)
        _TaxDesc = value
    End Set
End Property


Private _TaxGroupID As String
Public Property TaxGroupID() As String
    Get
        Return _TaxGroupID
    End Get
    Set(ByVal value As String)
        _TaxGroupID = value
    End Set
End Property


Private _TaxJurisdictionID As String
Public Property TaxJurisdictionID() As String
    Get
        Return _TaxJurisdictionID
    End Get
    Set(ByVal value As String)
        _TaxJurisdictionID = value
    End Set
End Property



Private _TaxCustCode As String
Public Property TaxCustCode() As String
    Get
        Return _TaxCustCode
    End Get
    Set(ByVal value As String)
        _TaxCustCode = value
    End Set
End Property


Private _TaxFreight As Boolean
Public Property taxFreight() As Boolean
    Get
        Return _TaxFreight
    End Get
    Set(ByVal value As Boolean)
        _TaxFreight = value
    End Set
End Property




Public Enum TaxableStatus
    All
    None
    some
End Enum


''' <summary>
''' It will first try to figure out if we're shipping to the same zip as the ship to
''' if it is the same, then we'll use the ship-tos tax group
''' if it is different, then we'll go to manual tax.
''' in manual tax, the customer record is reviewed and the class_1id field is interogated.
''' The code selected tells us what states the customer is taxable for.
''' If we are in those states, then the customer tax group is chosed based on the state.
''' </summary>
''' <param name="mCustCode"></param>
''' <param name="mShipToID"></param>
''' <param name="SubTotalAmt"></param>
''' <param name="FreightCost"></param>
''' <param name="FuelSurcharge"></param>
''' <param name="m_Ship_State_Province"></param>
''' <param name="m_Zip"></param>
''' <param name="TaxGroup"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Overridable Function wa_cc_CalcTax(ByVal mCustCode As String, _
                       ByVal mShipToID As String, _
                       ByVal SubTotalAmt As Double, _
                     ByVal FreightCost As Double, _
                    ByVal FuelSurcharge As Double, _
                    ByVal m_Ship_State_Province As String, _
                    ByVal m_Zip As String, _
                     Optional ByVal TaxGroup As String = "") As List(Of Tax)

    'do some 'normal' tax calcs.


    Return New List(Of Tax)

End Function

End Class


This is the CustomTax class that overrides the wa_cc_calctax function:

Public Class CustomTax

Inherits Tax


''' <summary>
''' It will first try to figure out if we're shipping to the same zip as the ship to
''' if it is the same, then we'll use the ship-tos tax group
''' if it is different, then we'll go to manual tax.
''' in manual tax, the customer record is reviewed and the class_1id field is interogated.
''' The code selected tells us what states the customer is taxable for.
''' If we are in those states, then the customer tax group is chosed based on the state.
''' </summary>
''' <param name="mCustCode"></param>
''' <param name="mShipToID"></param>
''' <param name="SubTotalAmt"></param>
''' <param name="FreightCost"></param>
''' <param name="FuelSurcharge"></param>
''' <param name="m_Ship_State_Province"></param>
''' <param name="m_Zip"></param>
''' <param name="TaxGroup"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Overrides Function wa_cc_CalcTax(ByVal mCustCode As String, _
                       ByVal mShipToID As String, _
                       ByVal SubTotalAmt As Double, _
                     ByVal FreightCost As Double, _
                    ByVal FuelSurcharge As Double, _
                    ByVal m_Ship_State_Province As String, _
                    ByVal m_Zip As String, _
                     Optional ByVal TaxGroup As String = "") As List(Of Tax)
    Dim lTX As New List(Of Tax)

    Dim mUseP21Tax As Boolean = True
    If mShipToID <= 0 Then
        mUseP21Tax = False
    End If

    If FreightCost <= 0 Then
        FreightCost = 0
    End If

    Dim tx As New CustomTax
    Dim ZipMatch As Boolean
    If mShipToID > 0 Then
        'we're dealing with a selected ship to so we should see if it all still matches
        ZipMatch = CheckZipAgainstShipTo(m_Zip, mCustCode, mShipToID)
    Else
        'this item is not a selected ship-to so no need to look for a match
        ZipMatch = False
    End If

    If ZipMatch = True Then

        lTX = LookupTaxForShipTo(mCustCode, mShipToID, SubTotalAmt, FreightCost)
    Else
        lTX = LookupManualTax(mCustCode, m_Ship_State_Province, SubTotalAmt, FreightCost, , m_Zip)


    End If


    Return lTX

End Function End Class


So the problem is in part: 1) If I make the class Tax into an Interface or an abstract class, then I have to 'new' it up as a new class inside the cartheader class so that I can call the wa_cc_lookupclass method. 2) When we new up a tax class inside the cartheader class, then we aren't newing up the custom instance of the tax class and therefore the custom code isn't used.

The goal is as follows: 1) Provide the base Tax class that has 'normal' functionality. 2) Allow a user to override the tax class in a web app's app_code folder to create new logic to calculate tax. It will still have the same input and output signatures as the original. 3) The cartheader.UpdateCartStep2 method must be able to access the wa_cc_calctax function from the base or overridden custom class (if it is overridden).

In this sample, we've clearly hacked it and passed in the custom version of the tax class (CustomTax) into the UpdateCartStep2 method as a parameter. This was a workaround we implemented based on a suggestion here... however we know it isn't the 'right' way to do it.

A: 

Something like the following:

abstract class BaseCartOperations
{
    public void SaveCart ()
    {

        // ...

        CalculateTax();
    }


    protected void CalculateTax ()
    {
        // Base Tax Stuff
        CalculateTaxInternal();
    }


    // Force implementation
    protected abstract void CalculateTaxInternal ();
}

? (or some variation thereof).

Noon Silk
I've tried this will little luck, but I think I'm going to revisit it.
Haldrich98
Challenge with the abstract class is that this is actually 2 classes. CartHead is a class and Tax is a class.CartHead can't instantiate an instance of tax if tax is an abstract class. Carthead needs access to the base class if the code doesn't get overridden. If I have the carthead class make a new instance of customtax (inherited from tax), then I'm back where I am. When carthead calls into the customtax class, it won't see that it has been overridden at the web project level, and will instead just use the customtax class that was new'd up in the carthead class.
Haldrich98
Not that it matters a lot, but in this instance calctax returens a list<of tax>. The tax class contains details such as jurisdiction name, percent, tax freight, tax amount, etc.
Haldrich98
+1  A: 

I can think of two options.

  1. Make your cart class an abstract class, and make the CalculateTax method an abstract method. This will force other developers to implement their own CalculateTax method and extend your base class.

  2. Pass a parameter to the SaveCart method (or the class constructor) that provides information on how to calculate tax, e.g. a delegate or function. Then the SaveCart method invokes that delegate to carry out the 'CalculateTax' part.

Kirk Broadhurst
For the short term, I've gone with a #2 variation... actually passing back in an instance of the custom class... it certainly is't the 'right' way to do it, but it did get the result I needed at the moment.
Haldrich98
You might be able to pass a Func parameter, rather than the entire instance. The Func (a function) defines what goes in to the function and what will come out of it. It's a delegate and will do exactly what you need. http://msdn.microsoft.com/en-us/library/bb549151.aspx
Kirk Broadhurst
I may end up going with the Func option, but it seems like there should be a cleaner way.
Haldrich98
I am curious for instances like this, who exactly does one call to get some assistance? It is great if you have a friend who is an expert or something, but I'd be will to pay someone to look at this one on one and help me figure the best direction. So, do you call Microsoft, or a local tech school? Any thoughts?
Haldrich98
You come to StackOverflow, or similar site. Or if you want to pay, you hire a consultant to come and help out!
Kirk Broadhurst
The best solution is probably to use an abstract base class with an abstract CalculateTax method. Is there any reason that this doesn't work for you?
Kirk Broadhurst
Hi Kirk, If I knew a good consultant to call, I'd do it, but locally, I don't know of anyone who has the skill to deal with this type of issue. I'd be needing someone who is at least an advanced developer... that's why I was thinking MS Tech Support (paid) might be the best option.
Haldrich98
The problem I have with the abstract class is that you must new up a new instance of the class... that means that since you've new'd up a new instance inside the library, it doesn't see the overridden instance that is created in the App_Code folder of the web project... Therefore anytime the carthead class calls the item, then it is referring to its own 'new' instance of the custom tax class, not the app_code custom instance.
Haldrich98
@haldrich98 fwiw with the appropriate tools you can do it online. Additionally there should be local consulting companies. Also note that if you are looking for a more consulting experience with MS, you will end up with their local consulting services / I'm not sure, but I don't think any other type of MS support would get involved so close if its not to deal with a specific technical issue. Another way to connect is with local .net dev groups.
eglasius
@haldrich98 "since you've new'd up a new instance inside the library, it doesn't see the overridden instance that is created in the App_Code folder" - Do you mean that you don't have a reference to the library that contains the custom class? I'm not sure I understand the problem.
Kirk Broadhurst
Thanks to everyone for the ideas and answers... Given our local .net group was about 5 people at last check, I decided to break down and get the Microsoft tech guys to put in their $.02.I'll post the final results when we get it resolved. The problem I had (even at MS) is that this isn't a question about a specific take, but more of a framework/.net question. I'll post back the recommended result they propose.
Haldrich98
A: 

After re-reading the question and your comments, I see that you want to solve 2 things:

  1. how to force the base class method to be called
  2. how to make the third party provided class involved in the system

Further below answer to the above questions, but I'd prefer instead:

Define an ITaxCalculator (you can find a better name ...). Receive an ITaxCalculator in the method that requires the calculation to be made. Provide a DefaultTaxCalculator or something similar if applicable, and either have the client call pass you that or use it as default if the calculator parameter is null.

If your code base makes it hard to pass that into the process, define a TaxCalculatorFactory like the CartFactory below, and only do TaxCalculatorFactory.Create() / if applicable by default it should be set to DefaultTaxCalculator.

Once you have an instance of ITaxCalculator, you call taxCalculator.CartTax(currentCart) to get the tax for the cart.

This way the extension point is much more focused, which imho makes it simpler at the end.


Answer to the original questions:

For 1, you can't force it directly. Its the subclass's decision to call base.TheSameMethod(), which allows it to put some logic before and/or after the base method is executed.

You can work around it by not having the logic you want to always be called in the method that's overrides. What you do is call 1 or 2 methods that can be overridden, for CartSave you could define OnCartSaving and OnCartSaved. CartSave can't be overridden, but as it calls those 2 that can be overridden you are providing the extension points.

For 2, I think this is whole new question, and there are various ways to go about it.

If you need to control when the instance is created, you can create a Factory, that can be configured by the client code (or through the app's .config) to use their class. Like the framework does for the different data providers.

A simple variation could be exposing a static property with a Func, that the client code can set to a function like:

CartFactory.Create = () => new MySpecificCart();

If its asp.net you could put that in the application start in global.asax. Then where you need to create a cart instance you can: CartFactory.Create();


Original answer - ignore / I didn't get the question at first:

Just a guess, but it seems that you are not declaring the method in the specific class with the override keyword.

If the above happens to be the case, you probably have a warning from the compiler.

eglasius
nope, confirmed that the 'custom' version of tax definitely uses the override... however that is declared in the web project that consumes the library. At this point the library would be compiled so the end user won't be able to go into the class directly in the class.
Haldrich98
@haldrich98 yes, I just re-read your question and I missed that the issue at hand is that you want to force the base's method to be called. You just need to change the approach, that said I just read in your comments that it alone doesn't solve your problem / since you want to know how to deal with it using the overridden version.
eglasius