I have an entity framework with a many-to-many relationship between Customers and Contacts.
I have generated a Domain Service Class and added the following method manually.
public Customer GetCustomerById(int Id)
{
return this.ObjectContext.Customer.Include("Contacts").SingleOrDefault(s => s.Id == Id);
}
I now want to create a page that shows me the customer details and a list of contacts associated with that customer.
I have the following in codebehind of the customerdetails.xaml to read the Id parameter that gets passed into the page.
public int CustomerId
{
get { return (int)this.GetValue(CustomerIdProperty); }
set { this.SetValue(CustomerIdProperty, value); }
}
public static DependencyProperty CustomerIdProperty = DependencyProperty.Register("CustomerId", typeof(int), typeof(CustomerDetails), new PropertyMetadata(0));
// Executes when the user navigates to this page.
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (this.NavigationContext.QueryString.ContainsKey("Id"))
{
CustomerId = Convert.ToInt32(this.NavigationContext.QueryString["Id"]);
}
}
I use the following xaml for the page:
<Grid x:Name="LayoutRoot" DataContext="{Binding ElementName=customerByIdSource, Path=Data}">
<riaControls:DomainDataSource Name="customerByIdSource" AutoLoad="True" QueryName="GetCustomerById">
<riaControls:DomainDataSource.QueryParameters>
<riaControls:Parameter ParameterName="Id" Value="{Binding ElementName=CustomerDetailsPage, Path=CustomerId}" />
</riaControls:DomainDataSource.QueryParameters>
<riaControls:DomainDataSource.DomainContext>
<sprint:Customer2DomainContext/>
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<StackPanel x:Name="CustomerInfo" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="3,3,3,3">
<TextBlock Text="Id"/>
<TextBox x:Name="idTextBox" Text="{Binding Id}" Width="160"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="3,3,3,3">
<TextBlock Text="Name"/>
<TextBox x:Name="nameTextBox" Text="{Binding Name}" Width="160"/>
</StackPanel>
<ListBox ItemsSource="{Binding Contact}" DisplayMemberPath="FullName" Height="100" />
</StackPanel>
</Grid>
When I do this the textboxes get nicely populated through the databinding, but the listbox remains empty.
Two questions:
Can I somehow specify the return
type of the GetCustomerById query,
so I can see the names when I
specify the binding through the
properties GUI?
What am I doing
wrong here? Why isn't my ListBox
populated? Am I going about this the correct way or do I need to set the databinding for the listbox in codebehind as well? If so, how? I haven't found how toaccess the Contacts property via the domain data source programmatically.
I use silverlight and entity framework 4.
I have found an answer to my question 2:
The association property Contacts is not included in the Domain Service Object types that are generated. You have to specify the [Include] attribute for them to be included. However the include attribute requires an [Association] attribute. You can not specify the [Association] attribute because this is a many to many relationship and the association attribute requires you to specify foreign keys.
The solution is to wrap your objects in a data transfer object (DTO). I did not have to make major changes to the code that was already in my question. The only thing changed was the retrieval of the Customer in the domain service class:
public CustomerDTO GetCustomerById(int Id)
{
return new CustomerDTO(this.ObjectContext.Customers.Include("Contacts").SingleOrDefault(s => s.Id == Id));
}
The main part of the solution was to change add the DTO classes to the underlying entity framework model:
[DataContract]
public partial class CustomerDTO : Customer
{
public CustomerDTO() { }
public CustomerDTO(Customer customer)
{
if (customer != null)
{
Id = customer.Id;
Name = customer.Name;
CustomerContacts = new Collection<ContactDTO>();
foreach (Contact d in customer.Contacts)
{
CustomerContacts.Add(new ContactDTO(d, Id));
}
}
}
[DataMember]
[Include]
[Association("CustomerContacts", "CustomerId", "Id")]
public Collection<ContactDTO> CustomerContacts
{
get;
set;
}
}
[KnownType(typeof(CustomerDTO))]
public partial class Customer
{
}
[DataContract()]
public partial class ContactDTO : Contact
{
public ContactDTO() { }
public ContactDTO(Contact contact, int customerId)
{
if (contact != null)
{
Id = contact.Id;
FullName = contact.FullName;
CustomerId = customerId;
}
}
[DataMember]
public int CustomerId { get; set; }
}
[KnownType(typeof(ContactDTO))]
public partial class Contact
{
}
The KnownType, DataMember and DataContract attributes were required to get this to work. In reality instantiating the objects will require a bit more copying of properties in the constructors. Is there an easy way to avoid code that does an explicit copy? I'm open for suggestions.
I was hoping to avoid the introduction of extra classes, but it seems unavoidable in case of a many to many relationship, because of the required Association attribute that needs a foreign key specification; in my case Contact.CustomerId.
Can anybody do better (== less coding) ?
Related
//My Model
public class BookInfo
{
public string BookName { get; set; }
public string BookDescription { get; set; }
}
//my View Model
public ObservableCollection<BookInfo> Bookmodel { get; set; }
public BookRepoInfo()
{
Bookmodel = new ObservableCollection<BookInfo> { //**is this correct way.**
new BookInfo { BookName = "Object-Oriented Programming in C#", BookDescription = "Object-oriented programming is a programming paradigm based on the concept of objects" },
......
};
}
XAML page:
<ContentPage.BindingContext>
<local:BookRepoInfo />
</ContentPage.BindingContext>
<X:YList ItemsSource="{Binding Bookmodel}"></X:YList>
Load the list item using MVVM pattern
Assuming that YList is either a inheritance from ListView or CollectionView, you'll need to provide some sort of template which you want to apply to each cell of that list.
Right now what is happening is that it will just call the ToString() on the object that you put in.
Change your code to be something like:
<X:YList ItemsSource="{Binding Bookmodel}">
<X:YList.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Label Text="{Binding BookName}"/>
<Label Text="{Binding BookDescription}"/>
</VerticalStackLayout>
</DataTemplate>
</X:YList.ItemTemplate>
</X:YList>
More information is here: https://learn.microsoft.com/dotnet/maui/user-interface/controls/collectionview/populate-data?view=net-maui-7.0#define-item-appearance
I have a simple model:
public sealed partial class ResultsModel : ObservableObject {
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(ClearCommand))]
[ObservableProperty]
ObservableCollection<Arrivals> _arrivals = new();
public RelayCommand SaveCommand { get; private set; }
public RelayCommand ClearCommand { get; private set; }
internal ResultsModel() {
SaveCommand = new RelayCommand(SaveRequest, CanSaveClear);
ClearCommand = new RelayCommand(OnClear, CanSaveClear);
}
public bool CanSaveClear() {
return _arrivals.Count > 0;
}
void OnClear() {
_arrivals.Clear();
}
async void SaveRequest() {
// save stuff
}
}
// c#
DataContext = (model = new ResultsModel());
...
model.Arrivals.insert(0, thing);
// The _arrivals are bound to an ItemsRepeater and appear in gui as //they're added
<ItemsRepeater ItemsSource="{Binding Arrivals}">
<Button Content="Clear" Command="{Binding ClearCommand}"/>
<Button Content="Save" Command="{Binding SaveCommand}" />
I've bound buttons to the two Commands and they work ok, I just can't work how to get the canExecute code to run more than one time.
I was expecting that when items get added to the _arrivals collection (and they do) the canExecutes would be re-evaluated via the NotifyCanExecuteChangedFor attribute, but I'm obviously missing some glue somewhere because the button are always disabled.
Any help would be appreciated.
It won't happen when you added an item to the Arrivals. But it will happen when you change the Arrivals by giving a new ObservableCollection. You could create a simple string property to test this behavior.
The reason for this behavior is that when the NotifyCanExecuteChangedFor Attribute is used, the IRelayCommand.NotifyCanExecuteChanged will be called when the setter of the property is called. In your scenario, that means only when the setter of the Arrivals property is called, this Attribute will call the IRelayCommand.NotifyCanExecuteChanged.
I'm sure that I'm missing some deep or obvious concept here :)
So I have a page now that can setup various Bluetooth sensors and get data from a heart rate monitor, speedometer and cadence sensor. (Using Plugin.BLE)
So I do all of that in a ViewModel for a ContentPage called BluetoothPage.
I want to display the data I get in a different ContentPage called DisplayPage.
I have created a simple class (model) that can hold the data I want:
namespace TSDZ2Monitor.Models;
public partial class BluetoothData : ObservableObject
{
//Heart rate raw data
public int HRM { get; set; }
public double HRR { get; set; }
//SPD raw data
public int SPDWheelRevolutions { get; set; }
public double SPDWheelEventTime { get; set; }
//CAD raw data
public int CADCrankRevolutions { get; set; }
public double CADCrankEventTime { get; set; }
}
So, how do I get the data from my Bluetooth page to my Display page?
I suspect I need to use an object based on my model and populate it with data in my Bluetooth viewmodel (easy...ish)?
But how can my Display page see this data as it happens?
When I tried working with ReactNative this sort of thing was a nightmare (State!)
Or am I being a bit simple in the head here :lol
Workaround: I could save the data to some local storage or sqlite as per https://learn.microsoft.com/en-us/learn/dotnet-maui/store-local-data/2-compare-storage-options - is that the way to do it, or can it be done with the object?
G.
Edit: I think I could also use the MessagingService https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/messagingcenter and https://codemilltech.com/messing-with-xamarin-forms-messaging-center/ if I can figure out how to use them in MVVM context.
Also What is the difference between using MessagingCenter and standard .NET event handlers for informing interested parties of changes?
So it seems that using the MessagingCenter was a way to go.
Following the guidance in https://codemilltech.com/messing-with-xamarin-forms-messaging-center/
I created a MessagingMarker class:
namespace TSDZ2Monitor.Classes;
public class MessagingMarker
{
}
That's all.
In the ViewModel where I wanted to send an object from, I did
MessagingCenter.Send(new MessagingMarker(), "BTDataUpdate", btd);
where btd was an instance of a class I created to hold my data: Here is a simplified model:
namespace TSDZ2Monitor.Models;
public partial class BluetoothData : ObservableObject
{
//Heart rate raw data
private int hRM;
public int HRM //heart rate
{
get => hRM;
set => SetProperty(ref hRM, value);
}
private double hRR; //heartrate R-R value
public double HRR
{
get => hRR;
set => SetProperty(ref hRR, value);
}
private double wheelRPM;
public double WheelRPM
{
get => wheelRPM;
set => SetProperty(ref wheelRPM, value);
}
private double cadence;
public double Cadence
{
get => cadence;
set => SetProperty(ref cadence, value);
}
}
In the constructor of the ViewModel for the sending page (probably best somewhere else?)
public BluetoothData btd = new();
This is not used in the XAML for this ViewModel
In my receiving ViewModel
I also created an instance of the BluetoothData class, but this is used in the XAML bindings
[ObservableProperty]
private BluetoothData bTData;
and in the constructor of the ViewModel I had
BTData = new BluetoothData();
MessagingCenter.Subscribe<MessagingMarker, BluetoothData>(this, "BTDataUpdate", (sender, arg) =>
{
//Debug.WriteLine($"Message received {arg}");
BTData.HRM = arg.HRM;
BTData.HRR = arg.HRR;
BTData.WheelRPM = arg.WheelRPM;
BTData.Cadence = arg.Cadence;
});
Well it works, don't know what the impact on performance might be, but it seems pretty responsive.
To my way of thinking though a more idea solution is to create a global instance of any class that any ViewModel can access.
I had need to also do this -- in my case, reference an ObservableCollection<State> States from multiple pages. Sharing this as another possible solution:
I created a class with the ObservableCollection as a static member which is populated once upon first use:
public class Filter
{
...
public static ObservableCollection<State> StateSelections { get; } = new();
...
public Filter(DataService dataService) : base()
{
this.dataService = dataService;
PopulateData();
}
public async Task PopulateData()
{
// Populate the available selections
await LoadStates();
}
public async Task LoadStates()
{
if (StateSelections?.Count > 0)
return;
...
}
}
For each page that uses the collection, its VM has a reference to an instance of the class:
public partial class ParkListVM : BaseVM
{
...
public Filter Filter { get; set; }
...
public void PopulateData()
{
if (ParkListVM.Filter is null)
ParkListVM.Filter = new Filter(dataService);
...
}
}
And the page has a reference to the static collection for display:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:NationalParks.ViewModels"
xmlns:model="clr-namespace:NationalParks.Models"
x:DataType="vm:ParkListVM"
x:Class="NationalParks.Views.ParkListPage"
Title="{Binding Title}">
<ContentPage.Resources>
<model:Filter x:Key="Filter"/>
</ContentPage.Resources>
...
<CollectionView ItemsSource="{Binding Source={x:Static model:Filter.StateSelections}}"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedStates}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:State">
<VerticalStackLayout>
<Label Style="{StaticResource LabelMedium}" Text="{Binding Name}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
...
</ContentPage>
I'm struggling with updating existing lecturer's data in database.
Every lecturer has Name, AcademicDegree and Courses which are taught by him/her (Courses==Lessons).
In fact there are more properties in class Lecturer but they are not relevant. For simplicity lets assume that POCO class is defined this way:
// POCO class (Entity Framework Reverse Engineering Code First)
public class Lecturer
{
public Lecturer()
{
this.Courses = new List<Course>();
}
public int Id_Lecturer { get; set; } // Primary Key
public string Name { get; set; }
public int? Academic_Degree_Id { get; set; }
public virtual AcademicDegree AcademicDegree { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
In Data Access Layer I have method void UpdateLecturer(Lecturer lecturer) which should update lecturer whose Id_Lecturer is equal to lecturer.Id_Lecturer (using lecturer.Name, lecturer.AcademicDegree and lecturer.Courses).
It's very handy method because in ViewModel I can call _dataAccess.UpdateLecturer(SelectedLecturer) (where SelectedLecturer is binded in XAML; SelectedLecturer properties can be set by user in TextBoxes and Checkbox).
Unfortunatelly this method:
public void UpdateLecturer(Lecturer lecturer)
{
using(var db = new AcademicTimetableDbContext())
{
// Find lecturer with Primary Key set to 'lecturer.Id_Lecturer':
var lect = db.Lecturers.Find(lecturer.Id_Lecturer);
// If not found:
if (lect == null)
return;
// Copy all possible properties:
db.Entry(lect).CurrentValues.SetValues(lecturer);
// Everything was copied except 'Courses'. Why?!
// I tried to add this, but it doesn't help:
// var stateMgr = (db as IObjectContextAdapter).ObjectContext.ObjectStateManager;
// var stateEntry = stateMgr.GetObjectStateEntry(lect);
// stateEntry.SetModified();
db.SaveChanges();
}
}
updates everything (i.e. Id_Lecturer, Name, Academic_Degree_Id and AcademicDegree) except Courses which are unchanged after db.SaveChanges().
Why? How can I fix it?
Similar problems:
updating-an-entity-in-entity-framework
how-do-can-i-attach-a-entity-framework-object-that-isnt-from-the-database
using-dbcontext-in-ef-feature-ctp5-part-4-add-attach-and-entity-states.aspx
entity-framework-updating-with-related-entity
entity-framework-code-first-no-detach-method-on-dbcontext
-- Edit --
I also tried this way (idea came from this post):
public void UpdateLecturer(Lecturer lecturer)
{
using (var db = new AcademicTimetableDbContext())
{
if (lecturer == null)
return;
DbEntityEntry<Lecturer> entry = db.Entry(lecturer);
if (entry.State == EntityState.Detached)
{
Lecturer attachedEntity = db.Set<Lecturer>().Find(lecturer.Id_Lecturer);
if (attachedEntity == null)
entry.State = EntityState.Modified;
else
db.Entry(attachedEntity).CurrentValues.SetValues(lecturer);
}
db.SaveChanges();
}
}
but still Courses don't overwrite old values.
-- Edit 2 --
In response to the #Slauma question I'll describe how I loaded SelectedLecturer (which is passed to UpdateLecturer(Lecturer lecturer) method as an argument).
I'm implementing MVVM concept so I have View project within my Solution with DataContext set to LecturerListViewModel. In View there is DataGrid with list of all lecturers fetched from databse. DataGrid is binded this way:
<DataGrid AutoGenerateColumns="False" Name="LecturersDataGrid" HeadersVisibility="Column" IsReadOnly="True" ItemsSource="{Binding Lecturers,Mode=TwoWay}" SelectedItem="{Binding SelectedLecturer, Mode=TwoWay}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
<DataGridTextColumn Header="Academic degree" Binding="{Binding AcademicDegree.Degree_Name}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="Edit" Click="EditButtonClick"/>
<Button Content="Delete" Command="{Binding DataContext.RemoveLecturer, ElementName=LecturersDataGrid}" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Lecturers are fetched from database in LecturerListViewModel constructor in this way:
///
/// Code within LecturerListViewModel class:
///
// All lecturers from database.
public ObservableCollection<Lecturer> Lecturers
// Constructor
public LecturerListViewModel()
{
// Call to Data Access Layer:
Lecturers = new ObservableCollection<Lecturer>(_dataAccess.GetAllLecturers());
// Some other stuff here...
}
private Lecturer _selectedLecturer;
// Currently selected row with lecturer.
public Lecturer SelectedLecturer
{
get { return _selectedLecturer; }
set { SetProperty(out _selectedLecturer, value, x => x.SelectedLecturer); }
}
///
/// Data Access Layer (within DataAccess class):
///
public IEnumerable<Lecturer> GetAllLecturers()
{
using (var dbb = new AcademicTimetableDbContext())
{
var query = from b
in dbb.Lecturers.Include(l => l.AcademicDegree).Include(l => l.Timetables).Include(l => l.Courses)
select b;
return query.ToList();
}
}
Setting the state to Modified and SetValues only updates scalar properties of the Lecturer entity. Updating the Courses collection (which is not a scalar property) needs more work. You must handle the cases that a course could have been removed from the collection, a course could have been added or the scalar properties of a course could have been modified.
Also the way to update the collection depends on a course being dependent on the lecturer or not. Does the course need to be deleted from the database when it has been removed from the collection or does only the relationship between lecturer and course need to be removed? Does a new course need to be created when it has been added to the collection or does only a relationship need to be established?
If no courses must be deleted and no new courses be created the Update method could look like this:
public void UpdateLecturer(Lecturer lecturer)
{
using(var db = new AcademicTimetableDbContext())
{
if (lecturer == null)
return;
var lecturerInDb = db.Lecturers
.Include(l => l.Courses)
.Single(l => l.Id_Lecturer == lecturer.Id_Lecturer);
// Update lecturer
db.Entry(lecturerInDb).CurrentValues.SetValues(lecturer);
// Remove courses relationships
foreach (var courseInDb in lecturerInDb.Courses.ToList())
if (!lecturer.Courses.Any(c => c.Id_Course == courseInDb.Id_Course))
lecturerInDb.Courses.Remove(courseInDb);
foreach (var course in lecturer.Courses)
{
var courseInDb = lecturerInDb.Courses.SingleOrDefault(
c => c.Id_Course == course.Id_Course);
if (courseInDb != null)
// Update courses
db.Entry(courseInDb).CurrentValues.SetValues(course);
else
{
// Add courses relationships
db.Courses.Attach(course);
lecturerInDb.Courses.Add(course);
}
}
}
db.SaveChanges();
}
Depending on the details of your scenario the correct solution might be slightly different.
Edit
If the courses in the lecturer.Courses collection have a reference to the lecturer (having a Lecturer navigation property maybe) you could have problems when you attach a course from this collection to the context because lecturerInDb is already attached and has the same key. You can try to change the last else block like so to solve the problem hopefully:
else
{
// Add courses relationships
var courseToAttach = new Course { Id_Course = course.Id_Course };
db.Courses.Attach(courseToAttach);
lecturerInDb.Courses.Add(courseToAttach);
}
An example, if I have a class named Order with a field referencing a Customer, and then an Order form with an drop down list (<%= Html.DropDownListFor(e => e.Customer.ID, new SelectList(...)) %>) for setting the Customer the model binder will create an empty Customer with only the ID set. This works fine with NHibernate but when validation is added to some of the fields of the Customer class, the model binder will say these fields are required. How could I prevent the model binder from validating these references?
Thanks!
Ages old question, but I figured I'd answer anyways for posterity. You need a custom model binder in this situation to intercept that property before it attempts to bind it. The default model binder will recursively attempt to bind properties using their custom binder, or the default one if not set.
The override you're looking for in DefaultModelBinder is GetPropertyValue. This is called over all properties in the model, and by default it calls back to DefaultModelBinder.BindModel - the entry point to the whole process.
Simplified model:
public class Organization
{
public int Id { get; set; }
[Required]
public OrganizationType Type { get; set; }
}
public class OrganizationType
{
public int Id { get; set; }
[Required, MaxLength(30)]
public string Name { get; set; }
}
View:
<div class="editor-label">
#Html.ErrorLabelFor(m => m.Organization.Type)
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.Organization.Type.Id, Model.OrganizationTypes, "-- Type")
</div>
Model Binder:
public class OrganizationModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
if (propertyDescriptor.PropertyType == typeof(OrganizationType))
{
var idResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Id");
if (idResult == null || string.IsNullOrEmpty(idResult.AttemptedValue))
{
return null;
}
var id = (int)idResult.ConvertTo(typeof(int));
// Can validate the id against your database at this point if needed...
// Here we just create a stub object, skipping the model binding and
// validation on OrganizationType
return new OrganizationType { Id = id };
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Note that in the View we create the DropDownListFor the model.foo.bar.Id. In the model binder, ensure that this is added to the model name as well. You can leave it off of both, but then DropDownListFor has a few issues finding the selected value without pre-selecting it in the SelectList you send it.
Finally, back in the controller, be sure to attach this property in your database context (if you're using Entity Framework, others might handle differently). Otherwise, it isn't tracked and the context will attempt to add it on save.
Controller:
public ActionResult Create(Organization organization)
{
if (ModelState.IsValid)
{
_context.OrganizationTypes.Attach(organization.Type);
_context.Organizations.Add(organization);
_context.SaveChanges();
return RedirectToAction("Index");
}
// Build up view model...
return View(viewModel);
}