cudreg-saml2/App_Code/Shared/FormulaEvaluator.vb
2025-06-08 16:22:20 +07:00

224 lines
10 KiB
VB.net

Imports Microsoft.VisualBasic
Imports System.IO
Imports BaseClasses.Data
Imports Ciloci.Flee
Imports Persons.UI
Imports Persons.Data
Namespace Persons
''' <summary>
''' The FormulaEvaluator class evaluates a formula passed to the Evaluate function.
''' You must set the DataSource and Page variables as part of this.
''' </summary>
''' <remarks></remarks>
Public Class FormulaEvaluator
Inherits BaseFormulaEvaluator
''' <summary>
''' Record control (or row) from which this evaluator is called. Could be Nothing
''' if called from the data access layer.
''' </summary>
Private _callingControl As Control = Nothing
Public Property CallingControl() As Control
Get
Return _callingControl
End Get
Set(ByVal value As Control)
_callingControl = value
End Set
End Property
''' <summary>
''' Create a new evaluator and prepare for evaluation.
''' </summary>
Public Sub New(Optional ByVal mainObj As Object = Nothing)
' pass mainObj to contructor so that formula like = mainObj.<functionName> can be evaluated
If mainObj Is Nothing Then
_evaluator = New ExpressionContext()
Else
_evaluator = New ExpressionContext(mainObj)
End If
' The order of adding types is important. First we add our own
' formula functions, followed by the generic types.
Evaluator.Imports.AddType(GetType(BaseFormulaUtils))
Evaluator.Imports.AddType(GetType(FormulaUtils))
' ADVANCED. For advanced usage, generic types can also be imported into
' the formula evaluator. This is done by adding types of some generic types
' such as Math, DateTime, Convert, and String. For example, if you add the
' Convert type, you can then use Convert.ToDecimal("string"). The second
' parameter to the AddType is the namespace that will be used in the formula.
' These functions expect a certain type. For example, Math functions expect
' a Double for the most part. If you pass a string, they will throw an exception.
' As such, we have written separate functions in FormulaUtils that are more
' loosely-typed than the standard libraries available here.
' Examples:
Evaluator.Imports.AddType(GetType(Math), "Math")
Evaluator.Imports.AddType(GetType(DateTime), "DateTime")
Evaluator.Imports.AddType(GetType(Convert), "Convert")
Evaluator.Imports.AddType(GetType(String), "String")
' We want a loosely-typed evaluation language - so do not
' type-check any variables.
Evaluator.Options.Checked = False
' Our policy is to always treat real numbers as Decimal instead of
' Double or Single to make it consistent across the entire
' evaluator.
Evaluator.Options.RealLiteralDataType = RealLiteralDataType.Decimal
' The variable event handler handles the variables based on the DataSource.
AddHandler _evaluator.Variables.ResolveVariableType, AddressOf variables_ResolveVariableType
AddHandler _evaluator.Variables.ResolveVariableValue, AddressOf variables_ResolveVariableValue
End Sub
''' <summary>
''' Evaluate the expression passed in as the string
''' </summary>
''' <param name="expression">The input whose absolute value is to be found.</param>
''' <returns>The result of the evaluation. Can we be any data type including string, datetime, decimal, etc.</returns>
Public Overrides Function Evaluate(ByVal expression As String) As Object
If IsNothing(expression) Then Return Nothing
If expression = "" Then Return ""
' Strip of the = in the front of the forumula - the Expression evaluator
' does not need it. Also, make sure to trim the result to remove any
' spaces in the front and the back.
expression = expression.TrimStart(New Char() {"="c, " "c}).Trim()
' Add all realted controls of this control. This includes the calling control, its children and parents.
AddRelatedControlVariables()
' If there are any exceptions when parsing or evaluating, they are
' thrown so that the end user can see the error. As such, there is no
' Try-Catch block here.
Try
Dim eDynamic As IDynamicExpression = _evaluator.CompileDynamic(expression)
Return eDynamic.Evaluate()
Catch ex As Exception
Return "ERROR: " & ex.Message
End Try
End Function
#Region "Private functions"
''' <summary>
''' Adds related record and table controls.
''' This has to navigate up to the page level and add any table or record controls.
''' And we have to navigate within to add any record/table controls.
''' And we have to go sideways to also add any controls.
''' Finally we have to add the page control.
''' This allows the expression to use any record or table control on the page
''' as long as it is accessible without being in a repeater.
''' </summary>
Private Sub AddRelatedControlVariables()
If IsNothing(CallingControl) Then Return
Try
' STEP 1: Our strategy is to first add the current control and
' all of its parents. This way, we maintain the full context of where
' we are. For example, if you are in a row within a table, within another row
' that is within another table, then by going up the hierarchy looking for parents
' will preserve all of the context.
' Later in Step 2 we will go through the other branches.
Dim ctl As Control = CallingControl
While Not (IsNothing(ctl))
If TypeOf ctl Is BaseApplicationRecordControl OrElse TypeOf ctl Is BaseApplicationTableControl Then
AddControlAndChildren(ctl)
End If
' Navigate up.
ctl = ctl.Parent
End While
' STEP 2: Go through the other branches on the page and add all other table and
' record controls on the page.
AddControlAndChildren(CallingControl.Page)
' STEP 3: Add more variable for ASCX control.
AddVariableNameWithoutUnderscore()
' STEP 4: Finally add the Page itself.
Evaluator.Variables.Add("Page", CallingControl.Page)
Catch ex As Exception
' Ignore and continue in case of a problem.
End Try
End Sub
''' <summary>
''' Add this control and all child controls of the given control.
''' We only add the Record and Table Controls. No other controls need
''' to be added.
''' This function is smart enough not to add or descend down a control
''' that was previously added by checking whether the Id is already contained
''' in the Evaluator variables. This avoids unnecessary traversal.
''' This function is called recursively to add any child controls.
''' </summary>
Private Sub AddControlAndChildren(ByVal ctl As Control)
' We quit immediately if a control is already in the list of variables,
' because we have convered that branch already.
Try
If IsNothing(ctl) Then Return
If Not (IsNothing(ctl.ID)) AndAlso Evaluator.Variables.ContainsKey(ctl.ID) Then Return
' If this is a record or table control, add it.
If TypeOf ctl Is BaseApplicationRecordControl OrElse TypeOf ctl Is BaseApplicationTableControl Then
If Not (IsNothing(ctl.ID)) Then
Evaluator.Variables.Add(ctl.ID, ctl)
End If
End If
For Each child As Control In ctl.Controls
' We do not want to go into a repeater because there will be multiple rows.
' So we will call AddChildControls only for those controls that are NOT repeaters.
If Not (TypeOf child Is System.Web.UI.WebControls.Repeater) Then
AddControlAndChildren(child)
End If
Next
Catch ex As Exception
' Ignore - we do not want to give an exception if we cannot add all variables.
End Try
End Sub
''' <summary>
''' If the current is not ASPX page but ASCX controls, the controls in this ASCX control has id starting with underscore.
''' To avoid confusion in formula, we also define variable name without underscore.
''' </summary>
''' <remarks></remarks>
Private Sub AddVariableNameWithoutUnderscore()
Dim vars As New Collections.Generic.Dictionary(Of String, Object)
Dim enumerator As Collections.Generic.IEnumerator(Of Collections.Generic.KeyValuePair(Of String, Object)) = Me.Evaluator.Variables.GetEnumerator()
While enumerator.MoveNext
If enumerator.Current.Key.StartsWith("_") Then
Dim varNameWitoutUnderscore As String = enumerator.Current.Key.Substring(1)
If Not Me.Evaluator.Variables.ContainsKey(varNameWitoutUnderscore) Then
vars.Add(varNameWitoutUnderscore, enumerator.Current.Value)
End If
End If
End While
Dim enumerator2 As Collections.Generic.Dictionary(Of String, Object).Enumerator = vars.GetEnumerator()
While enumerator2.MoveNext
Me.Evaluator.Variables.Add(enumerator2.Current.Key, enumerator2.Current.Value)
End While
End Sub
#End Region
End Class
End Namespace