Way to encapsulate methods from default.aspx.vb page to another class - asp.net

This question is about structure :
I have a Default.aspx page which holds references to (XML)services, and handles innerHTML of HTML objects. The ammount of buttons is based on services output.
Since this is an long and complex algorithm, I would like to encapsulate this in another class, to divide it into smaller and more readable chunks of code.
The problem is I do not know what the best option is, should I copy the reference of the used objects(service as well as the HTML items) into the new class ?
Since the ammount and origin of items it does not look to my like an elegant option.
I searched on the internet but could not find anything that suits this(I would think) common situation
This is the function I would like to transfer to another class. Currently it is in Default.aspx
and uses rep(ort)Service,defaultPath,path,selectionScreen and Image2 objects to draw the menu dynamically.
''' <summary>
''' Dynamically builds the square menu where the reports can be selected from.
''' It is based on the number of reports available
''' Show list of available reports on reporting server as HyperLinks
''' </summary>
''' <remarks></remarks>
Private Sub BuildReportSelectionArea(Optional path As String = "")
Dim items As CatalogItem() = repService.ListChildren(path, False)
Dim items2 As CatalogItem() = repService.ListChildren(path, False)
'Ensure that folders are shown first
Dim maxFolder = 0
For i = 0 To items.Count - 1 Step 1
If (items(i)).TypeName = "Folder" Then
items(i) = items2(maxFolder)
items(maxFolder) = items2(i)
maxFolder += 1
End If
' Some other code
End Sub
'TODO :Ensure the alfabetical order is preserved
Next

I would first generally comment on the code:
this means you are 2 times accessing the service, but the second array is later used to "sort" the items catalogItem, it seems a waste of resources to call the service twice
Dim items As CatalogItem() = repService.ListChildren(path, False)
Dim items2 As CatalogItem() = repService.ListChildren(path, False)
Reordering you could simply achieve using
Dim items As New List(Of CatalogItem)(RepService.ListChildren(path, False))
items.Sort(Function(item1 As CatalogItem, item2 As CatalogItem)
If String.Equals(item1.TypeName, item2.TypeName) Then
Return item1.FileName.CompareTo(item2.FileName)
End If
If item1.TypeName = "Folder" Then
Return -1
End If
Return 1
End Function)
which would sort by folders first, then by filename (you might have to update some of your properties to match)
you could further extract by either creating a module or a shared class that accepts the repService as an attribute and the path, and returns your output code
though creating a user control / webpart so you could add this functionality to each page you would like, would be a very good option as well, and the generally accepted way to refactor complex code...

Related

Can you access the ID of a Control added to an asp page in code-behind

I am adding nodes to a Treeview in an asp via code-behind. Is there a way of getting the id of the node after I've added it, example code:
e.Node.ChildNodes.Add()
How can I grab the id of the Node I'm adding (or added)? Just to be clear I'm trying to access the id that will be on the item on the page and could be used by client-code.
Code snippet of adding the Nodes:
Private Sub navTree_TreeNodePopulate(sender As Object, e As TreeNodeEventArgs) Handles navTree.TreeNodePopulate
Dim TestNode As New TreeNode
TestNode.Text = "Test"
e.Node.ChildNodes.Add(TestNode)
but .add has no return value
In VB, each TreeNode has a collection of child nodes in its ChildNodes property. The Add method returns void, so that's of no help in identifying the new node from your code behind. Microsoft's docs describe a TreeNode's Value property as "a non-displayed value used to store any additional data about the node, such as data used for handling postback events", and give an example of using it to store an ID (follow the link below to see their example)
Something like this might work in your case:
//to add the node
Private Sub navTree_TreeNodePopulate(sender As Object, e As TreeNodeEventArgs) Handles navTree.TreeNodePopulate
Dim TestNode As New TreeNode
TestNode.Text = "This is a test node."
TestNode.Value= 123 //your data's ID
e.Node.ChildNodes.Add(TestNode)
//to find the node by its ID later you'll have to loop through the collection and look for `Value == 123`
More info:
https://learn.microsoft.com/en-us/dotnet/api/system.web.ui.webcontrols.treenode.value?view=netframework-4.8

Setting values when a user updates a record using Entity Framework 4 and VB.NET in ASP.NET application

this is my first post, hopefully I'm following the rules!
After a few weeks of banging my head against brick walls and endless hours of internet searches, I have decided I need to make my own post and hopefully find the answers and guidance from all you experts.
I am building an ASP.NET application (in Visual Studio 2010) which uses an SQL 2008 database and Entity Framework 4 to join the two parts together. I designed the database as a database project first, then built the Entity Model as a class project which is then referenced in the main ASP.NET project. I believe this is called Database First in Entity Framwork terms?
I'm fairly new to Web Apps (I mainly develop WinForms apps) and this is my first attempt at using Entity Framwork.
I can just about understand C# so any code samples or snippets you might supply would be preferred in VB if possible.
To give some background on where I am so far. I built a regsitration page called register.aspx and created a wizard style form using ASP:Wizard with ASP:TextBox, ASP:DropdownList and ASP:CheckBoxList it's all laid out inside a HTML table and uses CSS for some of the formatting. Once the user gets the the last page in the wizard and presses "Finish" I execute some code in the VB code behind file as follows:
Protected Sub wizardRegister_FinishButtonClick(sender As Object, e As System.Web.UI.WebControls.WizardNavigationEventArgs) Handles wizardRegister.FinishButtonClick
Dim context As New CPMModel.CPMEntities
Dim person As New CPMModel.Person
Dim employee As New CPMModel.Employee
Dim newID As Guid = Guid.NewGuid
With person
.ID = newID
.UserID = txbx_UserName.Text
.Title = ddl_Title.SelectedValue
.FirstName = txbx_FirstName.Text
.MiddleInitial = txbx_MiddleInitial.Text
.FamilyName = txbx_FamilyName.Text
.Gender = ddl_Gender.SelectedValue
.DOB = txbx_DOB.Text
.RegistrationDate = Date.Now
.RegistrationMethod = "W"
.ContactMethodID = New Guid(ddl_ContactMethodList.SelectedValue)
.UserName = txbx_UserName.Text
If Not (My.Settings.AuthenticationUsingAD) Then
.Password = txbx_Password.Text ' [todo] write call to salted password hash function
End If
.IsRedundant = False
.IsLocked = False
End With
context.AddToPeople(person)
With employee
.ID = newID
.PayrollNumber = txbx_PayrollNumber.Text
.JobTitle = txbx_JobTitle.Text
.DepartmentID = ddl_DepartmentList.SelectedValue
.OfficeNumber = txbx_OfficeNumber.Text
.HomeNumber = txbx_HomeNumber.Text
.MobileNumber = txbx_MobileNumber.Text
.BleepNumber = txbx_BleepNumber.Text
.OfficeEmail = txbx_OfficeEmail.Text
.HomeEmail = txbx_HomeEmail.Text
.IsRedundant = False
.RedundantDate = Nothing
'--------------------------
.FiledBy = Threading.Thread.CurrentPrincipal.Identity.Name
.FiledLocation = My.Computer.Name
.FiledDateTimeStamp = Date.Now
'----------------------------
End With
context.AddToEmployees(employee)
context.SaveChanges()
End Sub
The above works fine, seemed to be a sensible way of doing it (remember I'm new to Entity Framework) and gave me the results I extpected.
I have another page called manage.aspx on this page is a tab control, each tab page contains a asp:DetailsView which is bound to an asp:EntityDataSource, I have enabled Update on all the Entity Data Sources and on some I have also enabled Insert, none of the have delete enabled.
If I build and run the app at this stage everything works fine, you can press the "edit" link make changes and then press "update" and sure enough those updates are displayed on the screen instantly and the database does have the correct values in.
Here's the problem, I need to intercept the update in the above code (the bold bit) notice there is a column in my database called FiledBy, FiledLocation, and FiledDateTimeStamp. I don't want to show these columns to the user viewing the ASP.NET page but I want to update them when the user presses update (if there were any changes made). I have tried converting the ASP:DetailsView into a template and then coding the HTML side with things like;
<## Eval(User.Name) #>
I did manage to get it to put the correct values in the textboxes when in edit mode but I had the following problems;
It didn't save to the database
I have to show the textboxes which I don't want to do
I have read on several other posts on here and other websites that you can override the SaveChanges part of the Entity Framwork model one example of this is in the code below;
public override int SaveChanges()
{
var entities = ChangeTracker.Entries<YourEntityType>()
.Where(e => e.State == EntityState.Added)
.Select(e => e.Entity);
var currentDate = DateTime.Now;
foreach(var entity in entities)
{
entity.Date = currentDate;
}
return base.SaveChanges();
}
The problem is, that whilst I understand the idea, and can just about transalate it to VB I have no idea how to implement this. Where do I put the code? How do I call the code? etc.
I would ideally like to find a solution that meets the following requirements:
Doesn't get overwritten if I was to regenerate / update the Entity Model
Doesn't need to be copy and pasted for every table in my model (most but not all have the audit columns in)
Can be used for other columns, for example, if a user make a selection in a check list I may want to write some value into another (not exposed to the user) column.
Am I taking the right approach to displaying data in may webpages I certainly find the DataView and GridView limiting, I did think about creating a form like the registration form in table tags but have no Idea how to populate the textboxes etc with values from the database nor how to implement paging as many of the tables have one to many relationships, although saving changes would be easy if I did populate textboxes on a table.
Thank you all in advance to anyone who offers there support.
Kevin.
Ok so I have now got closer, but still not got it working correctly. I've used the following code to edit the value in the DetailsView on the Databound event. If writes the correct timestamp into the read only textbox but does not right this value back to the database when you press the Update button on the DetailsView control.
Protected Sub dv_Employee_DataBound(sender As Object, e As EventArgs) Handles dv_Employee.DataBound
If dv_Employee.CurrentMode = DetailsViewMode.Edit Then
For Each row In dv_Employee.Rows
If row.Cells(0).Text = "FiledDateTimeStamp" Then
row.Cells(1).Text = Date.Now()
End If
Next
End If
End Sub
I like the idea of overriding the save changes method. Your context should be a partial class, so you just need to define another class marked as partial with the same name in the same namespace and add the override to it.
Namespace CPMModel
Partial Public Class CPMEntities
...Your override here
End Class
Since this is an override of the default SaveChanges method you do not need to make any changes in how it is called. You will need to find a way to gain access to entity level properties.
Since this is the method that commits changes for all entities you will not be able to access the properties like you do in your example, since the entity class does not define an of your concrete properties. So you need to create partial classes for your entities and have them all implement an interface that defines the properties you would like to interact with.
Create interface:
Interface IDate
Property Date() as DateTime
End Interface
Create partial class for each of your entities that implements the IDate interface. (The same rules apply for these partial classes they must be marked partial, have the same name as the class they extend and live in the same namespace) Then in your SaveChanges override
public override int SaveChanges()
{
var entities = ChangeTracker.Entries<Entity>()
.Where(e => e.State == EntityState.Added)
.Select(e => e.Entity);
var currentDate = DateTime.Now;
foreach(var entity in entities)
{
***Now cast entity to type of IDate and set your value ***
entity.Date = currentDate;
}
return base.SaveChanges();
}
I don't write in Vb so I left the syntax in C#.
That should fulfill all of your requirements.
Ok, so this isn't exactly how I imagined it would work but it has infact met my needs. Kind of!
What I did was Write a Protected Sub in the code behind file of the aspx file.
Protected Sub CustomEntityUpdate_Employee(sender As Object, e As
EntityDataSourceChangingEventArgs)
Dim emp As CPMModel.Employee = e.Entity
' Check if the entity state is modified and update auditing columns
If emp.EntityState = EntityState.Modified Then
emp.FiledDateTimeStamp = Date.Now()
emp.FiledBy = Threading.Thread.CurrentPrincipal.Identity.Name
emp.FiledLocation = My.Computer.Name
End If
End Sub
Then in the aspx page I edited the Entity datasource code to call the above sub routine
#nUpdating="CustomEntityUpdate_Employee"
<asp:EntityDataSource
ID="eds_Employee"
runat="server"
ConnectionString="name=CPMEntities"
DefaultContainerName="CPMEntities"
EnableFlattening="False"
EnableUpdate="True"
EntitySetName="Employees"
AutoGenerateWhereClause="True"
Where=""
EntityTypeFilter="Employee"
**OnUpdating="CustomEntityUpdate_Employee">**
<WhereParameters>
<asp:SessionParameter
Name="ID"
SessionField="UserID" />
</WhereParameters>
</asp:EntityDataSource>
The Protected Sub, checks if the entity has been modified and if it has updates the entity auditing columns with the values I wanted.
It works but it will certainly make for a lot more code, I ideally would like a way to package this up perhaps into a class and just call the sub from the various aspx pages via the OnUpdating event and have the class figure out which entity was making the call and handle all the logic there.

Setting AspxComboBox.SelectedIndex not working after using LinqDataSource.Selecting. Any ideas?

I've posted this on the DevExpress forums as well, but you can't beat stackoverflow for good answers.
I seem to be having a problem with the DevExpress AspxComboBox control.
I'm using DevExpress 9.1.11 controls with Visual Studio 2008.
Here are my controls:
<asp:LinqDataSource ID="ContactsDataSource" runat="server"
ContextTypeName="DAL.MorrisDataContext"
Select="new (Id, FullName)"
TableName="Contact">
</asp:LinqDataSource>
<dxe:ASPxComboBox ID="SignerComboBox" runat="server" ToolTip="Select a Contact to use."
AutoPostBack="True" DataSourceID="ContactsDataSource" TextField="FullName"
ValueField="Id" ValueType="System.String" Width="140px" SelectedIndex="0">
</dxe:ASPxComboBox>
I am handling the Selecting event of the LinqDataSource to filter the list of items used in the ComboBox as follows:
Protected Sub ContactsDataSource_Selecting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.LinqDataSourceSelectEventArgs) Handles ContactsDataSource.Selecting
Dim Contacts As IList(Of Contact) = iEntity.Contacts.ToList
e.Result = Contacts
End Sub
iEntity.Contacts is just a LINQtoSQL object containing a child collection of LINQtoSQL objects known as "Contacts". All of this works perfectly.
The problem comes when I try to add a new Contact to the collection of contacts for the Entity and then try to update the ComboBox to reflect the addition as follows:
Private Sub SignerUpdate()
Dim m_Last = SignerContactLastTextBox.Text
Dim m_First = SignerContactFirstTextBox.Text
If m_Last <> "" OrElse m_First <> "" Then
Dim m_Middle = SignerContactMiddleTextBox.Text
Dim m_Suffix = SignerContactSuffixTextBox.Text
Dim m_ContactIndex As Int32 = SignerComboBox.SelectedIndex
Dim m_Contact As Contact = New Contact
If m_Last <> "" Then m_Contact.LastName = New Identifier With {.Value = m_Last}
If m_First <> "" Then m_Contact.FirstName = New Identifier With {.Value = m_First}
If m_Middle <> "" Then m_Contact.MiddleName = New Identifier With {.Value = m_Middle}
If m_Suffix <> "" Then m_Contact.Suffix = New Identifier With {.Value = m_Suffix}
iEntity.Contacts.Add(m_Contact)
SignerComboBox.DataBind()
SignerComboBox.SelectedIndex = SignerComboBox.Items.Count - 1
'SignerComboBox.SelectedIndex = 3
Else
SignerContactSuffixTextBox.ErrorText = "Must have First or Last name."
SignerContactSuffixTextBox.IsValid = False
End If
End Sub
The key lines being these:
iEntity.Contacts.Add(m_Contact)
SignerComboBox.DataBind()
SignerComboBox.SelectedIndex = SignerComboBox.Items.Count - 1
'SignerComboBox.SelectedIndex = 3
The first line adds a new Contact to the collection.
The second line rebinds the ComboBox which results in the Selecting event from above being fired and getting the updated list including the new contact.
Everything is still working fine. I can put a watch on SignerCombBox.Items and see that the new item is there after the bind. It was not there prior to the bind.
All is good.
Then we come to the third line. This is where we have a problem. I expect this to select the last item in the list of items. The results of SignerComboBox.Items.Count is correct. We get a count back that does include the new item.
What doesn't work, is that when we try to use that result to set the SelectedIndex property. It will not accept it.
The fourth line is where I've tried to do everything manually to make sure I'm not losing my mind. Turns out I'm not. As you can see this line is commented out.
Even though I have added another line to Items and I can see that line in Items. It can not be set as selected using SelectedIndex. When I try to set it, it just remains set to whatever it was set to previously (in my case 0). I've also tried setting it with SignerComboBox.SelectedItem = SignerComboBox.Items(SignerComboBox.Items.Count - 1) or SignerComboBox.SelectedItem = SignerComboBox.Items(3) to no avail.
So, for example. With my test data I start out with 3 items in the collection. 0 - 2 are in Items.
At this point I can set SignerComboBox.SelectedIndex to 0, 1, or 2. It works just fine.
I then add another item.
Rebind to get the updated Items.
Check the Items. They now contain 4 items. 0, 1, 2, and 3
Set SignerComboBox.SelectedIndex = 3. It will not work. It will remain 0. Even though I know that item is in the Items list of the control.
Set SignerComboBox.SelectIndex = 2. Works just fine. It will be set to 2.
It's as if it's deciding whether or not it's a valid index based on something other than it's own Items list.
I even tried inserting the new one higher in the list and using an index that had already existed. Once that had been done though, that index would not be accepted.
I really don't understand it. I'm pretty sure it must be a bug. Even that doesn't make much sense to me though, because it seems like the kind of bug that would have been reported about a million times over already. So my only guess is that perhaps it has something to do with the fact that I'm handling the Selecting event of the DataSource and giving it a custom result. I don't see why that would matter, but I can't see how anything else is the least bit out of the ordinary.
I've been pounding on this problem for a while now and I'm out of ideas. So any help would be greatly appreciated.
Thanks,
Tory
EDIT:
I thought maybe I could phrase the question a bit more succintly.
Here's what it comes down to:
Bind a combobox to a LinqDataSource based on a LinqToSql object that
is related as a child to another LinqToSql object.
Create an instance of the parent object.
Use the LinqDataSource.Selecting event to use the contents of the
parent object instance's child object collection, as the results of
the LinqDataSource in the form of an IList(Of ChildObject).
Show the contents of the ComboBox.
Add a new Child object to the child object collection of the parent
object instance.
Databind the ComboBox again in order to pick up the newly added
member of the collection.
Attempt to set the SelectedIndex of the ComboBox to the index of the
newly created member of it's contents, using something like this:
SignerComboBox.SelectedIndex = SignerComboBox.Items.Count - 1
You will not be able to.
It will show up in the ComboBox.Items list, but you will not be able
to assign it's index to the SelectedIndex property.

Entity Framework: Insists on adding new entity in many-to-many instead of re-using existing FK

I have got a many to many relationship, briefly
Cases -----< CaseSubjectRelationships >------ CaseSubjects
More fully:
Cases(ID, CaseTypeID, .......)
CaseSubjects(ID, DisplayName, CRMSPIN)
CaseSubjectsRelationships(CaseID, SubjectID, PrimarySubject, RelationToCase, ...)
In my many-to-many link table are additional properties relating to the subject's association with the specific case - such as, start date, end date, free-text relationship to case (observer, creator, etc)
An Entity Framework data model has been created - ASP.NET version 4.0
I have a WCF service with a method called CreateNewCase which accepts as its parameter a Case object (an entity created by the Entity Framework) - its job is to save the case into the database.
The WCF service is invoked by a third party tool. Here is the SOAP sent:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<CreateNewCase xmlns="http://tempuri.org/">
<c xmlns:a="http://schemas.datacontract.org/2004/07/CAMSModel">
<a:CaseSubjectsRelationships>
<a:CaseSubjectsRelationship>
<a:CaseSubject>
<a:CRMSPIN>601</a:CRMSPIN>
<a:DisplayName>Fred Flintstone</a:DisplayName>
</a:CaseSubject>
<a:PrimarySubject>true</a:PrimarySubject>
<a:RelationToCase>Interested</a:RelationToCase>
<a:StartDate>2011-07-12T00:00:00</a:StartDate>
</a:CaseSubjectsRelationship>
<a:CaseSubjectsRelationship>
<a:CaseSubject>
<a:CRMSPIN>602</a:CRMSPIN>
<a:DisplayName>Barney Rubble</a:DisplayName>
</a:CaseSubject>
<a:RelationToCase>Observer</a:RelationToCase>
<a:StartDate>2011-07-12T00:00:00</a:StartDate>
</a:CaseSubjectsRelationship>
</a:CaseSubjectsRelationships>
<a:CaseType>
<a:Identifier>Change of Occupier</a:Identifier>
</a:CaseType>
<a:Description>Case description</a:Description>
<a:Priority>5</a:Priority>
<a:QueueIdentifier>Queue One</a:QueueIdentifier>
<a:Title>Case title</a:Title>
</c>
</CreateNewCase>
</s:Body>
</s:Envelope>
The WCF engine deserializes this into a Case entity for me correctly and when I look in the debugger everything is set up properly.
What I want to do, is only create a new CaseSubject if there is not already an entry in the database with that CRMSPIN specified (CRMSPIN is a reference number from a central customer database)
So, in the below example, I want to see if I already have an entry in CaseSubjects for somebody with CRMSPIN 601 and if I do, I don't want to create another (duplicate) entry but instead make the new case link to the existing subject (although a new row will need, obviously, need creating in CaseSubjectsRelationships with the specific 'additional' information such as relationship etc)
Here is the .NET code I have tried to do this.
Public Class CamsService
Implements ICamsService
Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase
Using ctx As New CAMSEntities
' Find the case type '
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
' Give an error if no such case type '
If ct Is Nothing Then
Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString))
End If
' Set the case type based on that found in database: '
c.CaseType = ct
For Each csr In c.CaseSubjectsRelationships
Dim spin As String = csr.CaseSubject.CRMSPIN
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
If Not s Is Nothing Then
' The subject has been found based on CRMSPIN so set the subject in the relationship '
csr.CaseSubject = s
End If
Next
c.CreationChannel = "Web service"
c.CreationDate = Now.Date
' Save it '
ctx.AddToCases(c)
ctx.SaveChanges()
End Using
' Return the case reference '
Return c.ID.ToString
End Function
End Class
As you can see, instead the For Each loop, I try to get a subject based on the CRMSPIN and if I get something, then I update the "CaseSubject" entity. (I have also tried csr.SubjectID = s.ID instead of setting the whole entity and also I have tried setting them both!).
However, even when putting a breakpoint on the ctx.SaveChanges() line and looking at how the subjects are set up and seeing in the debugger that it looks fine, it is always creating a new row in the CaseSubjects table.
I can see in principle this should work - you'll see I've done exactly the same thing for Case Type - I have picked the identifier sent in the XML, found the entity with that identifier via the context, then changed the case's .CaseType to the entity I found. When it saves, it works perfectly and as-expected and with no duplicated rows.
I'm just having trouble trying to apply the same theory to one side of a many-to-many relationship.
Here are some (hopefully relevant) extracts from the .edmx
<EntitySet Name="Cases" EntityType="CAMSModel.Store.Cases" store:Type="Tables" Schema="dbo" />
<EntitySet Name="CaseSubjects" EntityType="CAMSModel.Store.CaseSubjects" store:Type="Tables" Schema="dbo" />
<EntitySet Name="CaseSubjectsRelationships" EntityType="CAMSModel.Store.CaseSubjectsRelationships" store:Type="Tables" Schema="dbo" />
<AssociationSet Name="FK_CaseSubjectsRelationships_Cases" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_Cases">
<End Role="Cases" EntitySet="Cases" />
<End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" />
</AssociationSet>
<AssociationSet Name="FK_CaseSubjectsRelationships_CaseSubjects" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_CaseSubjects">
<End Role="CaseSubjects" EntitySet="CaseSubjects" />
<End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" />
</AssociationSet>
EDIT: The property setters for the CaseSubject property of the CaseSubjectsRelationships object:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
<XmlIgnoreAttribute()>
<SoapIgnoreAttribute()>
<DataMemberAttribute()>
<EdmRelationshipNavigationPropertyAttribute("CAMSModel", "FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject")>
Public Property CaseSubject() As CaseSubject
Get
Return CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value
End Get
Set
CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value = value
End Set
End Property
You didn't specify what context model are you working with, so I'll assume you're using the default (ie. you don't have some explicit .tt files to generate your entities).
So, basically, this is what I think is happening.
In your code, when you fetch something from context:
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
this ct is in context. The method argument that you deserialized from service (the c) is not in context. You can regard the context as the "object tracking and fetching" entity, that makes sure that everything attached to it can know about any changes, if it's new, deleted etc.
So, when you get to the part:
' Set the case type based on that found in database: '
c.CaseType = ct
at the moment you assign something that's attached to something not attached, the unattached object will get pulled into context as well - there can't be "partially" attached entities - if it's attached, everything it references has to be attached as well. So, this is the moment where the c gets "dragged" into the context (implicitly). When it enters the context, it will get marked as "new" since it doesn't know anything about it yet (it has no knowledge of it, no change tracking info...).
So, now that everything about that object c is in context, when you query the context for this:
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
it will figure that indeed there is an object with that CRMSPIN and it's already attached - "hey, no need to go to database, I already have this!" (trying to be smart and avoid a db hit), and it will return your own object.
Finally, when you save everything, it will be saved, but your attached c and all of it's child objects that are marked as 'new' will be inserted instead of updated.
The easiest fix would be to first query everything you need from context, and only then start assigning it to properties of your object. Also, take a look at UpdateCurrentValues, it may also be helpful...
OK: So the resolution to this was a combination of what #veljkoz said in his answer (which was very useful to help me out to reach the final resolution, but on its own was not the full resolution)
By moving the For Each loop to the first thing done before anything else (As hinted by #veljkoz), that got rid of the Collection was modified, enumeration may not continue error I was getting when I set csr.CaseSubject = Nothing.
It also turned out to be important to not attach entities (e.g. not to set csr.CaseSubject to an entity but only to Nothing) but instead to use the .SubjectID property. A combination of all the above led me to the following code, which works perfectly and doesn't try to insert duplicate rows.
+1 to #veljkoz for the assist but also note that the resolution includes setting the entity reference to Nothing and using the ID property.
Full, working code:
Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase
Using ctx As New CAMSEntities
' Subjects first, otherwise when you try to set csr.CaseSubject = Nothing you get an exception '
For Each csr In c.CaseSubjectsRelationships
Dim spin As String = csr.CaseSubject.CRMSPIN
Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)
If Not s Is Nothing Then
' The subject has been found based on CRMSPIN so set the subject in the relationship '
csr.CaseSubject = Nothing
csr.SubjectID = s.ID
End If
Next
' Find the case type '
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)
' Give an error if no such case type '
If ct Is Nothing Then
Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString))
End If
' Set the case type based on that found in database: '
c.CaseType = ct
c.CreationChannel = "Web service"
c.CreationDate = Now.Date
' Save it '
ctx.AddToCases(c)
ctx.SaveChanges()
End Using
' Return the case reference '
Return c.ID.ToString
End Function

vb.net alternative to select case when dealing with object values

Hey all, I was able to do this via a SELECT CASE statement, however I'm always trying to improve my code writing and was wondering if there was a better approach. Here's the scenario:
Each document has x custom fields on it.
There's y number of documents
However there's only 21 distinct custom fields, but they can obviously have n different combinations of them depending on the form.
So here's what I did, I created an object called CustomFields like so:
Private Class CustomFields
Public agentaddress As String
Public agentattorney As String
Public agentcity As String
Public agentname As String
Public agentnumber As String
Public agentstate As String
Public agentzip As String
... more fields here ....
End Class`
Then I went ahead and assigned the values I get from the user to each of those fields like so:
Set All of Our Custom Fields Accordingly
Dim pcc As New CustomFields()
pcc.agentaddress = agent.address1
pcc.agentattorney = cplinfo.attorneyname
pcc.agentcity = agent.city
pcc.agentname = agent.agencyName
pcc.agentnumber = agent.agentNumber
pcc.agentstate = agent.state
pcc.agentzip = agent.zip ....other values set to fields etc.
Now the idea is based upon what combo of fields come back based upon the document, we need to assign the value which matches up with that custom field's value. So if the form only needed agentaddress and agentcity:
'Now Let's Loop Through the Custom Fields for This Document
For Each cf As vCustomField In cc
Dim customs As New tblCustomValue()
Select Case cf.fieldname
Case "agentaddress"
customs.customfieldid = cf.customfieldid
customs.icsid = cpl.icsID
customs.value = pcc.additionalinfo
Case "agentcity"
customs.customfieldid = cf.customfieldid
customs.icsid = cpl.icsID
customs.value = pcc.additionalinfo
End Select
_db.tblCustomValues.InsertOnSubmit(customs)
_db.SubmitChanges()
This works, however we may end up having 100's of fields in the future so there a way to somehow "EVAL" (yes I know that doesn't exist in vb.net) the cf.fieldname and find it's corresponding value in the CustomFields object?
Just trying to write more efficient code and looking for some brainstorming here. Hopefully my code and description makes sense. If it doesn't let me know and I'll go hit my head up against the wall and try writing it again.
If I am reading your question correctly, you are trying to avoid setting the value of fields, when the field isn't used. If so, I would recommend you just go ahead and set the field to nothing in that case.

Resources