Use linq to combine 2 classes into a 3rd collection - vb.net

If I have 2 classes and want to take 2 properties from each class, and combine them into a separate collection, how would this be done using linq?
Say the 2 classes are:
class guitars
public ID
public title
public manufacturer
end class
class drums
public ID
public title
public manufacturer
end class
I tried this, but it didn't work:
Private Interface Instruments
Property ID As String
Property name As String
End Interface
Dim results = From item In guitars _
Select New Instruments() With _
{ _
.ID = item.ID, _
.name = item.Title _
}
Dim results2 = From item In drums _
Select New Instruments() With _
{ _
.ID = item.ID, _
.name = item.Title _
}
Dim combined = results + results2

You can make it much simpler than that.
Just make both classes implement the IInstrument interface.
You can then write
guitars.Cast(Of IInstrument)().Concat(drums)

Union or Concat depending on wether or not you want to allow duplicates in the list.
http://msdn.microsoft.com/en-us/library/vstudio/bb763068.aspx
Dim Combined = results.Union(results2) ' no duplicates allowed
Dim Combined = results.Concat(results2) 'duplicates allowed
EDIT: Added Concat at the behest of MatthewWhited. He is totally right. The list is distinct according to the MSDN.

Related

Putting Excel Tables into a Custom Class

I'm new to using VBA classes, and I think what I want to do -- using them with ListObjects -- is more of an "intermediate" than "beginner" technique.
Let's say I have two tables related to cars.
tblCarDesc
ID MAKE MODEL DOORS ENGINE
1 Chevrolet Corvette 2 V8
2 Ford Escort 4 V6
3 Rolls-Royce SilverCloud 4 V8
tblCarProd
ID COUNTRY TYPE
1 US Sport
2 US Economy
3 UK Luxury
(The same cars are in both tables, and shown by the ID numbers.)
I want to have a class called objCars that includes the fields (columns) from both tables. That way, when referring to Car #3, objCars.Make would be "Rolls-Royce" and objCars.Type would be "Luxury".
1) Is there a way to import both tables into objCars?
Perhaps I would create an array big enough to hold all the columns, then load both tables into it. The tutorial I've been reading says that I would then create a Collection, loop through each row of the array, make a new instance of objCars, and assign objCars.Make, objCars.Model, etc., for each row. Then each item of the Collection would contain a car. (Or something like that. I don't really know much about Collections either.) If that's right, is it the best way?
2) How exactly does one refer to a specific car? The examples I've read like to loop through Collections and work on each item therein, but what if I want to extract a particular item? I know the Ford is Car #2; how do I get the objCars.Make and objCars.Model for that particular ID number?
I would have two classes. A class clsCar for one car and a class clsCars for a collection of cars.
Each of this classes may have setter and getter methods and also may have custom methods if needed. Especially the clsCars should have a set of getBy...-methods to get a car or a collection of cars from the collection by criterion.
Example:
clsCar:
Private pID As Long
Private pMAKE As String
Private pMODEL As String
Private pDOORS As Integer
Private pENGINE As String
Private pCOUNTRY As String
Private pTYPE As String
Public Property Get ID() As Long
ID = pID
End Property
Public Property Let ID(Value As Long)
pID = Value
End Property
Public Property Get MAKE() As String
MAKE = pMAKE
End Property
Public Property Let MAKE(Value As String)
pMAKE = Value
End Property
Public Property Get MODEL() As String
MODEL = pMODEL
End Property
Public Property Let MODEL(Value As String)
pMODEL = Value
End Property
Public Property Get DOORS() As Integer
DOORS = pDOORS
End Property
Public Property Let DOORS(Value As Integer)
pDOORS = Value
End Property
Public Property Get ENGINE() As String
ENGINE = pENGINE
End Property
Public Property Let ENGINE(Value As String)
pENGINE = Value
End Property
Public Property Get COUNTRY() As String
COUNTRY = pCOUNTRY
End Property
Public Property Let COUNTRY(Value As String)
pCOUNTRY = Value
End Property
Public Property Get CarTYPE() As String
CarTYPE = pTYPE
End Property
Public Property Let CarTYPE(Value As String)
pTYPE = Value
End Property
Public Function toString() As String
toString = pID & "; " & _
pMAKE & "; " & _
pMODEL & "; " & _
pDOORS & "; " & _
pENGINE & "; " & _
pCOUNTRY & "; " & _
pTYPE
End Function
clsCars:
Private pCars As collection
Private Sub Class_Initialize()
Set pCars = New collection
End Sub
Public Sub add(oCar As clsCar)
pCars.add oCar
End Sub
Public Function getByIndex(lIndex As Long) As clsCar
Set getByIndex = pCars.Item(lIndex)
End Function
Public Function getByID(lID As Long) As clsCar
Dim oCar As clsCar
For Each oCar In pCars
If oCar.ID = lID Then
Set getByID = oCar
End If
Next
End Function
Public Function getByEngine(sEngine As String) As collection
Dim oCar As clsCar
Set getByEngine = New collection
For Each oCar In pCars
If oCar.ENGINE = sEngine Then
getByEngine.add oCar
End If
Next
End Function
default Module:
Public oCars As clsCars
Sub initialize()
Dim oCar As clsCar
Dim oListObject As ListObject
Dim oListRow As ListRow
Dim oCells As Range
Set oCars = New clsCars
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarDesc")
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
Set oCar = New clsCar
oCar.ID = oCells(, 1).Value
oCar.MAKE = oCells(, 2).Value
oCar.MODEL = oCells(, 3).Value
oCar.DOORS = oCells(, 4).Value
oCar.ENGINE = oCells(, 5).Value
oCars.add oCar
Next
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarProd")
Dim lID As Long
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
lID = oCells(, 1).Value
Set oCar = oCars.getByID(lID)
If Not oCar Is Nothing Then
oCar.COUNTRY = oCells(, 2).Value
oCar.CarTYPE = oCells(, 3).Value
End If
Next
MsgBox oCars.getByIndex(2).toString
For Each oCar In oCars.getByEngine("V8")
MsgBox oCar.toString
Next
End Sub
I would use a class for each, and an array of each also, so arrCars holds clsCars, and arrProd holds clsProduction. I would then use the index of each for each array when populating, so arrCars(1)=Corvette and arrProd(1)=US Sport then from each you can refer to the others, so if x=3, cars(x) and prod(x) will be correct. Or use a vlookup in excel first, and make one larger table, with the need for only 1 ID then, if that is the way they are related, but can a Bentley also be 3. Its not quite clear what you mean by the 2nd table, is there an entry for each car, or is it like catgeorising the car further, just using a certain selection. Another idea would be to have an extra property in the car class, of ProductionID and then use a static production "table" of classes to refer to.

How to assign linq anonymous type variable

I have a linq query that gives me an System.Collections.Generic.IEnumerable(of <Anonymous Type>). Actually I wanted to create a class to get better access to what I want form that query but I fail aready with declaring a member variable that should hold the query outcome. The query I use in the Sub New() of my class is (explained and c# version here:)
Dim jan1 = New DateTime(DateTime.Today.Year, 1, 1)
Dim startOfFirstWeek = jan1.AddDays(1 - CInt(jan1.DayOfWeek))
Dim weeks = Enumerable.Range(0, 54).[Select](Function(i) New With { _
Key .weekStart = startOfFirstWeek.AddDays(i * 7) _
}).TakeWhile(Function(x) x.weekStart.Year <= jan1.Year).[Select](Function(x) New With { _
x.weekStart, _
Key .weekFinish = x.weekStart.AddDays(4) _
}).SkipWhile(Function(x) x.weekFinish < jan1.AddDays(1)).[Select](Function(x, i) New With { _
x.weekStart, _
x.weekFinish, _
Key .weekNum = i + 1 _
})
My Class should look like:
Public Class WeekInfo
Private _weeks as ?????
end Class
Could anyone tell me what is the usual procedure to accomplish that task and how to put the find a type for my member to access the weeks variable?
VB.Net anonymous types are exposed as type Object so you're variable would be:
Dim weeks As IEnumerable(Of Object)
You will not get intellisense on the anonymous type's fields outside of the function you created them in but you can access them dynamically, i.e.
For Each week In weeks
Console.WriteLine(week.weekStart + " " + week.weekFinish)
Next
If you need to expose the type externally you should define a concrete class or struct instead.
Make your WeekInfo class look like this:
Public Class WeekInfo
Public Property WeekStart As Date
Public Property WeekEnd As Date
Public Property WeekNumber As Int32
End Class
And then load it with you linq query as list of your WeekInfo(s):
Dim jan1 = New DateTime(DateTime.Today.Year, 1, 1)
Dim startOfFirstWeek = jan1.AddDays(1 - CInt(jan1.DayOfWeek))
Dim weeks = Enumerable.Range(0, 54).Select(Function(i) New With { _
Key .weekStart = startOfFirstWeek.AddDays(i * 7) _
}).TakeWhile(Function(x) x.weekStart.Year <= jan1.Year).[Select](Function(x) New With { _
x.weekStart, _
Key .weekFinish = x.weekStart.AddDays(4) _
}).SkipWhile(Function(x) x.weekFinish < jan1.AddDays(1)).[Select](Function(x, i) New WeekInfo With { _
.WeekStart = x.weekStart, _
.WeekEnd = x.weekFinish, _
.WeekNumber = i + 1 _
}).ToList()

How to bind a DataGridView to a list of custom classes

I have a list of custom classes that I am building using a TableAdapter.
I want to add these to a DataGridView binding certain columns only.
I have tried the code below to fill and bind the data:
lAllBookings = (From r As DataRow In BookingsTableAdapter1.GetDataWithItems().Rows
Select New Booking With {.bookingID = r.Item("BookingID"), _
.itemID = r.Item("ItemID"), _
.bookedOutDate = r.Field(Of DateTime?)("BookedOutDate"), _
.bookedInDate = r.Field(Of DateTime?)("BookedInDate"), _
.identType = r.Item("IdentType"), _
.identString = r.Item("IdentString"), _
.image = r.Item("Image"), _
.complete = r.Item("Complete"), _
.notes = r.Item("Notes"), _
.itemName = r.Item("ItemName"), _
.itemBC = r.Item("ItemBarcode")}).ToList
dgvBookings.Columns("BookingID").DataPropertyName = "bookingID"
dgvBookings.Columns("ItemIdent").DataPropertyName = "itemName"
dgvBookings.Columns("BookedOut").DataPropertyName = "bookedOutDate"
dgvBookings.Columns("IdentString").DataPropertyName = "identString"
dgvBookings.DataSource = lAllBookings
Now when I do this I get the correct number of rows but all fields are blank.
I've run through a few questions on SO and a few tutorials but they all seem to do things slightly different to what I need.
Is there a way I can fill the DataGridView using my list of items?
I'd rather avoid using a DataSet if I can as I've built a lot of other code on this List<Of Class> type.
Edit - Here is the Class Booking declaration:
Public Class Booking
Public bookingID As Integer
Public itemID As Integer
Public itemName As String
Public itemBC As String
Public identType As Short
Public identString As String
Public image As Byte()
Public complete As Boolean
Public notes As String
Public bookedInDate As DateTime?
Public bookedOutDate As DateTime?
End Class
I know is really late for you but maybe it can help someone.
I had the same problem that you but using vb.net.
Finally I found the solution: Your class was not exposing properties but variables. The binding system looks for properties and can't find them so you get the empty rows.
Try to complete your class adding {get; set;} (or the Property attribute if using vb.net) and everything will work.
Hope it can be helpful.
I am not sure that you are getting in IAllBookings all the information you want. In any case, you are not passing it rightly to the dgvBookings. Here you have a couple of small codes to help you to understand how this works better:
dgvBookings.Columns.Clear()
Dim newTable As New DataTable
newTable.Columns.Add("Column1")
newTable.Columns.Add("Column2")
newTable.Columns.Add("Column3")
newTable.Rows.Add("1", "2", "3")
newTable.Rows.Add("1", "2", "3")
newTable.Rows.Add("1", "2", "3")
dgvBookings.DataSource = newTable
The newTable emulates perfectly the DataGridView structure (columns & rows) and thus it can be given as a DataSource directly.
Unlikely the case of a simple List:
dgvBookings.Columns.Clear()
Dim newList = New List(Of String)
newList.Add("1")
newList.Add("2")
newList.Add("3")
dgvBookings.DataSource = newList
You are providing less information than expected (1D vs. the expected 2D) and thus the result is not the one you want. You need to provide more information; for example: instead of relying on DataSource, you might add the rows one by one:
dgvBookings.Columns.Add("Column1", "Column1")
For Each item In newList
dgvBookings.Rows.Add(item)
Next
I hope that this answer will help you to understand better how to deal with DataGridView and with the different data sources.
-- UPDATE
Row by row option applied to your specific case.
For Each item As Booking In lAllBookings
With item
dgvBookings.Rows.Add(.bookingID.ToString(), .itemID.ToString(), .bookedOutDate.ToString(), .identString.ToString())
End With
Next

How can I use Linq to entities to group a collection without using anonymous types?

The documentation has an example about grouping on multiple properties:
Dim custRegionQuery = From cust In db.Customers _
Group cust.ContactName By Key = New With _
{cust.City, cust.Region} Into grouping = Group
For Each grp In custRegionQuery
Console.WriteLine(vbNewLine & "Location Key: {0}", grp.Key)
For Each listing In grp.grouping
Console.WriteLine(vbTab & "{0}", listing)
Next
Next
Suppose I have a real type (i.e. not anonymous) that has properties for each element of the key, as well as a property for the group, so something a little like:
Public Class CustomerRegionGroup
Public Sub New(city As String, region as String, customers As IEnumerable(Of Customer))
Me.City = city
Me.Region = region
Me.Customers = customers
End Sub
Public Property City As String
Public Property Region As String
Public Property Customers As IEnumerable(Of Customer)
End Class
Is it possible to rewrite the original query to just return IEnumerable(Of CustomerRegionGroup), or do I have to use the anonymous type, and run a second query on the result of the first?
Turns out it was a lot simpler than some of the things I'd tried, as I didn't appreciate that you could just do:
Dim custRegionQuery = From cust In db.Customers _
Group By cust.City, cust.Region _
Into Group _
Select New CustomerRegionGroup(City, Region, Group)

LINQ query to group data between two list collections, populating a nested object (VB.Net)

I have these objects:
Public Class MakeInfo
Public Property Name As String
Public Property Description As String
Public Property Stock As StockInfo
End Class
Public Class ModelInfo
Public Property Make As String
Public Property Name As String
Public Property Description As String
Public Property Stock As StockInfo
End Class
Public Class StockInfo
Public Property Count As Integer
Public Property MinPrice As Double
End Class
Using LINQ I need to take a List(Of MakeInfo) and a List(Of ModelInfo) and aggregate the StockInfo from ModelInfo into the List(Of MakeInfo).
So for each MakeInfo I will have a total count of all stock where MakeInfo.Name = ModelInfo.Make, and also a minimum price.
I think it's going to be something like this, but I'm having trouble accessing the nested object and not sure if it's going to be possible with a single query. I've tried a few variations, but here's where I'm up to:
newList = From ma In makeData _
Group Join mo In modelData _
On ma.Name Equals mo.Make _
Into Group _
Select New MakeInfo With {.Name = m.Name, _
.Description = m.Description, _
.Stock = ?}
If you are actually trying to modify the original MakeInfo objects, you should stop the query with the group. newList will have each make paired with the group of models of that make. You can iterate through newList, and for each make, aggregate the models into the StockInfo for the make.
Dim newList = From ma In makeData _
Group Join mo In modelData _
On ma.Name Equals mo.Make _
Into Group
For Each g In newList
g.ma.Stock = g.Group.Aggregate(
New StockInfo() With {.MinPrice = Double.NaN},
Function(si, model)
Return New StockInfo With {
.Count = si.Count + 1,
.MinPrice = If(Double.IsNaN(si.MinPrice) OrElse
model.Stock.MinPrice < si.MinPrice,
model.Stock.MinPrice,
si.MinPrice)}
End Function)
Next
If you actually want new instances, just change the loop to create a new item instead of modifying the existing make.
Dim results As New List(Of MakeInfo)()
For Each g In newList
Dim newMake As New MakeInfo()
newMake.Name = g.ma.Name
newMake.Description = g.ma.Description
newMake.Stock = g.Group.Aggregate( same as before )
results.Add(newMake)
Next
I eventually found the right way to do this and it doesn't require loop iteration:
Dim new_makes = From ma In makes
Join mo In models On mo.Make Equals ma.Name
Group By ma.Name, ma.Description
Into TotalCount = Sum(mo.Stock.Count), LowestPrice = Min(mo.Stock.MinPrice)
Select New MakeInfo With {.Name = Name, _
.Description = Description, _
.Stock = New StockInfo With {.Count = TotalCount, .MinPrice = LowestPrice}}
I was certain that it would be possible.