LINQ Query based on user preferences - asp.net

How can I do this better (so it actually works : )
I have a LINQ Query that includes an order by that is based on a user preference. The user can decide if they would like the results ordered asc or desc.
If fuserclass.SortOrder = "Ascending" Then
Dim mydat = (From c In dc.ITRS Order By c.Date Ascending Select c)
Else
Dim mydat = (From c In dc.ITRS Order By c.Date Descending Select c)
End If
For each mydata in mydat ***<<<error "mydat is not declared"***
I know I could put my For Each loop inside the If and Else, but that seems silly to have the same code twice. I know you know of a better way : )

Define Dim mydat before the If statement. It is out of scope when your code reaches the for loop.

Use an extension method, and function-based LINQ (my VB is rusty as hell)
VB.NET:
<ExtensionAttribute> _
Public Shared Function OrderByString(Of TSource, TKey) ( _
source As IEnumerable(Of TSource), _
keySelector As Func(Of TSource, TKey) _
strType As String) As IOrderedEnumerable(Of TSource)
If strType = "Ascending" Then
return source.OrderBy(keySelector)
Else
return source.OrderByDescending(keySelector)
End If
End Function
C#.NET:
public static IOrderedEnumerable<TSource> OrderByString(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
string strType)
{
if (strType == "Ascending")
return source.OrderBy(keySelector);
return source.OrderByDescending(keySelector);
}
Then call your query like this:
Dim mydat = dc.ITRS.OrderByString(Function(item) item.Date, fuserclass.SortOrder)
Or on C#:
var mydat = dv.ITRS.OrderbyString(item => item.Date, fuserclass.SortOrder);

Related

Access VB property based on name as string - Fastest Option

I'm developing an ASP.NET MVC web app in VB and I am required to output a set of data to a table format, and to allow the user to configure the order and presence of columns from an available set. The data set is stored as a list of the object type representing the row model.
Currently, I implement this using CallByName. Iterating over an ordered list of property names and outputting the value from the instance of the row model. However, based on testing this seems to be a major bottleneck in the process.
I've seen a recommendation to store delegates to get the property, against the string representation of the property's name. So, I can presumably do something like this:
Public Delegate Function GetColumn(ByRef RowObj As RowModel) As String
Dim GetPropOne As GetColumn = Function(ByRef RowObj As RowModel) RowObj.Prop1.ToString()
Dim accessors As New Hashtable()
accessors.Add("Prop1", GetPropOne)
Then, loop through and do something like this:
Dim acc As GetColumn = accessors(ColumnName)
Dim val As String = acc.Invoke(currentRow)
It looks faster, but it also looks like more maintenance. If this is indeed faster, is there a way I can dynamically build something like this? I'm thinking:
Public Delegate Function GetObjectProperty(Instance As Object) As Object
For Each prop In GetType(RowModel).GetProperties()
Dim acc As GetObjectProperty = AddressOf prop.GetValue
columns.Add(prop.Name, acc)
Next
Dim getColVal As GetObjectProperty = columns(ColumnName)
Dim val As String = getColVal.Invoke(currentRow).ToString()
Open to suggestions for different approaches.
I do a similar thing to turn a SOAP response into a Data Table
Public Function ObjectToDataSource(objName) As DataSet
Dim CollName = ""
Dim ds As New DataSet()
For Each m As System.Reflection.PropertyInfo In objName.GetType().GetProperties()
If m.CanRead Then
If InStr(m.PropertyType.ToString, "[]") <> 0 Then
CollName = m.Name
Exit For
End If
End If
Next
Dim CollObj
CollObj = CallByName(objName, CollName, CallType.Get)
If CollObj.length = 0 Then
Call EndTask("No Supply Chains to display", "Royal Mail failed to return Supply Chain information for these credentials", 3)
Else
Dim dt_NewTable As New DataTable(CollName)
ds.Tables.Add(dt_NewTable)
Dim ColumnCount = 0
For Each p As System.Reflection.PropertyInfo In CollObj(0).GetType().GetProperties()
If p.CanRead Then
If p.Name <> "ExtensionData" Then
dt_NewTable.Columns.Add(p.Name, p.PropertyType)
ColumnCount = ColumnCount + 1
End If
End If
Next
Dim rowcount = CollObj.Length - 1
For r = 0 To rowcount
Dim rowdata(ColumnCount - 1) As Object
For c = 0 To ColumnCount - 1
rowdata(c) = CallByName(CollObj(r), dt_NewTable.Columns.Item(c).ToString, CallType.Get)
Next
dt_NewTable.Rows.Add(rowdata)
rowdata = Nothing
Next
End If
Return ds
End Function
This is specific to my needs in terms of getting CollName and not requiring ExtensionData
If ColumnName is the same name as one of the RowModel's properties I don't see why you need the long workaround with delegates...
An extension method which gets only the property you want right now is both faster and consumes less memory.
Imports System.Runtime.CompilerServices
Public Module Extensions
<Extension()> _
Public Function GetProperty(ByVal Instance As Object, ByVal PropertyName As String, Optional ByVal Arguments As Object() = Nothing) As Object
Return Instance.GetType().GetProperty(PropertyName).GetValue(Instance, Arguments)
End Function
End Module
Example usage:
currentRow.GetProperty("Prop1")
'or:
currentRow.GetProperty(ColumnName)

How i can remove the argument or arguments from the procedure call

When I run my code I get a notification that
Public readonly property Count as integer has no parameters and it's
return type cannot be indexed.
How can I fix this?
Public Sub Connect(userName As String)
Dim id = Context.ConnectionId
If ConnectedUsers.Count(Function(x) x.ConnectionId = id) = 0 Then
ConnectedUsers.Add(New UserDetail() With { _
Key .ConnectionId = id, _
Key .UserName = userName _
})
' send to caller
Clients.Caller.onConnected(id, userName, ConnectedUsers, CurrentMessage)
' send to all except caller client
Clients.AllExcept(id).onNewUserConnected(id, userName)
End If
End Sub
Change your If statement as suggested by Plutonix. But he was not giving you the full code:
If ConnectedUsers.Where(Function(x) x.ConnectionId = id).Count() = 0 Then
When you access Count on the List type, it's only a property, not a method. The Where extension method allows you to pass in the lambda check, and returns you a different object that has a Count method (not property) like the one you were expecting to see.

How to handle null values in LINQ with multiple where clauses

I have a LINQ query with multiple values in a where clause. The values in where clause are from Filtering options from checkboxlist. Checkboxlist can return Null (or empty string in the e.g.) and that means I don't need where clause in the query as selecting none on checkboxlist means selecting all. I didn't know how to write one good LINQ that can handle that so I ended up using multiple IF statements with multiple queries as below. I tested it and works fine for now with 2 parameters in the where clause. But in reality, I need more parameters to pass in to the query and it will get messy having many IF statements to do that job. How can I handle that in one good LINQ query?
Function FilterCol(ByVal col As List(Of ProductDetails), Optional SelectedCategory As List(Of String) = Nothing, Optional SelectedBrand As List(Of String) = Nothing) As List(Of ProductDetails)
Dim strSelectedCategory As String = String.Join(",", SelectedCategory .ToArray())
Dim strSelectedBrand As String = String.Join(",", SelectedBrand .ToArray())
If strSelectedCategory = "" And strSelectedBrand = "" Then
Return col
ElseIf strSelectedCategory = "" Then
Dim res1 As IEnumerable(Of StatsDetails) = From x In col Where strSelectedBrand.Contains(x.Brand) Select x
Return res1.ToList
ElseIf strSelectedBrand = "" Then
Dim res2 As IEnumerable(Of StatsDetails) = From x In col Where strSelectedCategory.Contains(x.Category) Select x
Return res2.ToList
Else
Dim res As IEnumerable(Of StatsDetails) = From x In col Where strSelectedCategory.Contains(x.Category) And strSelectedBrand.Contains(x.Brand) Select x
Return res.ToList
End If
End Function
If you have multiple variable conditions for your LINQ query, consider using Dynamic LINQ Library. It will allows you to combine the conditions into a string variable in advance based on supplied parameters and then use that variable as condition for LINQ Query - this is something akin to Dynamic SQL in SQL Server.
This is easier to do using Extension Method syntax. I don't think doing it as one LINQ statement is a good idea.
What you can do is first create your base query using just the table. I'm going to do it in C#, but it won't be complex to translate to VB--I don't want to confuse you or myself!
var baseQuery = col;
Now that you've got your base table, you can start adding wheres based on your conditions.
if(condition1 == true)
{
baseQuery = baseQuery.Where(this is your where condition);
}
if(condition2 == true)
{
baseQuery = baseQuery.Where(this is another condition);
}
So, you can chain your Wheres together, allowing you to narrow down your query based on the conditions.
Finally, you return your query's results, with the ToList. This avoids the code duplication that you had in your conditions and everything's easier to understand and maintain.
Use lambda exp like this:
Dim FnValueExists =
Function(v As String, a As List(Of String)) a Is Nothing OrElse
a.Count = 0 OrElse
a.Contains(v)
Then in your linq, just test for the value like below:
Dim res As IEnumerable(Of StatsDetails) =
From x In col
Where FnValueExists(x.Category, SelectedCategory) And
FnValueExists(x.Brand, SelectedBrand)
Select x
The main code that determines if item x will be selected or not, is that a Is Nothing OrElse a.Count = 0 OrElse a.Contains(v) line in the FnValueExists lambda expression.
In the case above, if SelectedCategory and/or SelectedBrand list is Nothing or empty (Count=0), or contains the value, the item will still be selected.
So if users did not select any categories, you can set SelectedCategory to Nothing or just an empty list, therefore FnValueExists(x.Category, SelectedCategory) will always return true.

Using Results Outside of dbContext in VB.NET

Lets say I have code like this:
Using dbContext as mydb_entities = New mydb_entities
Dim qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
End Using
Using dbContext as yourdb_entities = New yourdb_entities
Dim qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p)
End Using
How can I compare the results of qperson to qyou? Since the results "disappear" once End Using is executed?
You will need to declare both variables outside the using statements
Dim qperson As IQueryable(Of Person)
Dim qyou As IQueryable(Of Customer)
Using dbContext as mydb_entities = New mydb_entities
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
End Using
Using dbContext as yourdb_entities = New yourdb_entities
qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p)
End Using
The key thing here is to know when your LINQ queries are going to get run. When this line of code is executed:
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p)
no query is sent to the server. Instead you get back an object that implements the IQueryable(Of T) interface that describes what the query is. The query isn't actually sent to the server and executed until you start to use the results, such as in a For Each loop. This is called delayed execution and is fundamental to LINQ.
So what does this mean to you? Well, it means that the context must not be disposed before you execute the query. In the examples so far this is not necessarily always true. (The nested answer might do so, depending what is actually happening inside the nested usings.)
The typical way to deal with this is to force execution of the query to produce an in-memory collection of the results before the context is disposed. The ToList() extension method is a common way to do this. So, for example:
Dim qperson As IList(Of Person)
Dim qyou As IList(Of Customer)
Using dbContext as mydb_entities = New mydb_entities
qperson = (From p in dbContext.People _
Where p.name = "John" _
Select p).ToList()
End Using
Using dbContext as yourdb_entities = New yourdb_entities
qyou = (From p in dbContext.Customer _
Where p.name = "John" _
Select p).ToList()
End Using
Now you have executed the queries and got the results into memory before the contexts are disposed and you can happily do what you want with them.
In C# I just typically nest the usings...
using (var context blahentities())
{
using (var context2 blahentities())
{
}
}
Check this out for nesting usings in vb...
Nested using statements

ASP.NET MVC, LINQ and UpdateModel issues

Ok - I have two tables in the database, employees and companies. Here are the basic schemas for each
Table Companies
CompanyID - GUID - PK
CompanyName - NVarChar(100)
Other Descriptive Fields....
Table Employees
EmployeeID - GUID - PK
CompanyID - GUID - FK
Employee Descriptive Fields...
So now I have a one to many relationship as each company can have multiple employees. I have also created a dataRepository for my employee class with the following functions:
Public Function GetEmployee(ByVal company As String, ByVal id As Guid) As DB.EmpWithComp.employee
Dim emp As DB.EmpWithComp.employee = (From e In db.employees Where e.employeeID = id And e.company.companyName= company Select e).Single
Return emp
End Function
Public Sub save()
db.SubmitChanges()
End Sub
Everything so far is working great. The issue comes when I need to edit an employee. Here are my controller functions
<AcceptVerbs(HttpVerbs.Post)> _
Function Edit(ByVal id As Guid, ByVal company As String, ByVal formValues As FormCollection) As ActionResult
Dim e As New DB.EmpWithComp.employee
e = db.GetEmployee(company, id)
Try
UpdateModel(e)
db.save()
Return RedirectToAction("Index")
Catch ex As Exception
Return View(e)
End Try
End Function
Function Edit(ByVal id As Guid, ByVal company As String) As ActionResult
Dim e As New DB.EmpWithComp.employee
e = db.GetEmployee(company, id)
If Not e Is Nothing Then
Return View(e)
Else
Return View("~/Views/Admin/Employees/NotFound.aspx")
End If
End Function
The employee is found, the edit form is populated and the post function is firing as it should. However, my updateModel fails with no real explanation. I traced the code going through the updateModel and all attributes are being assigned the correct values. However, when the updateModel gets to the following section in the LinqToSql class, the value for company is Nothing and this is what is causing the failure
<Association(Name:="company_employee", Storage:="_company", ThisKey:="companyID", IsForeignKey:=true)> _
Public Property company() As company
Get
Return Me._company.Entity
End Get
Set
Dim previousValue As company = Me._company.Entity
**If ((Object.Equals(previousValue, value) = false) _
OrElse** (Me._company.HasLoadedOrAssignedValue = false)) Then
Me.SendPropertyChanging
If ((previousValue Is Nothing) _
= false) Then
Me._company.Entity = Nothing
previousValue.employees.Remove(Me)
End If
Me._company.Entity = value
If ((value Is Nothing) _
= false) Then
value.employees.Add(Me)
Me._companyID = value.companyID
Else
**Me._companyID = CType(Nothing, System.Guid)**
End If
Me.SendPropertyChanged("company")
End If
End Set
End Property
What am I missing with the FK relationship? The previous value for company is set to the correct value going into the UpdateModel, but the current value is not set. If I manually set each property (e.firstname = request("firstname"), etc) and call my save method on the dataRepository or remove the relationship and use UpdateModel, everything works properly. I would rather use the UpdateModel as it makes the code cleaner, etc. Sorry for the lengthy post, but this is driving me nuts. Any ideas?
BTW, I am not trying to change the FK, just simply update the employee name.
how does your view look like?
like this?
if yes, UpdateModel would try to write the string in request("Company") to e.Company which isn`t a string but an object.
what you really want to change is the Employee.CompanyID right? you can do this by using a Hidden Input with the new ID or if you want to find the CompanyID by the CompanyName you could write your own ModelBinder.
EDIT
oh you dont event want to update the company, sry my fault.
in that case you need to tell UpdateModel to ignore the company which is present in your POST
Function Edit(ByVal id As Guid, ByVal company As String...
you can do this by setting company as excluded property like this
Dim exclude() As String = {"company"}
UpdateModel(e, String.Empty, Nothing, exclude)
or what i prefer, you could start using prefixs for your fields like this
<%=Html.Textbox("Employee.Firstname", ViewData.Model.Firstname)>%
and call UpdateModel like this
UpdateModel(e, "Employee")
hth

Resources