-
May 15th, 2017, 10:34 AM
#1
Thread Starter
Lively Member
[RESOLVED] WPF Having Trouble with binding a Datagrid control on load.
Good day,
I am very new to programming. There are hundreds of forum posts and articles on how to bind a datagrid and I am trying my damnedest to mimic them but it is not working for me.
I am just making a To Do List as practice to help me learn .NET. I have a database table called "Tasks" that I want to load any records assigned to the user(There is only 1 record for testing it right now).
In my ViewModel code, I have a subroutine that queries the database and loads it into a list(of task)
Code:
Public Class TaskViewModel
Public Property GridView As List(Of Task)
Public Sub New()
End Sub
Public Sub CallTaskData(username As String)
Dim taskData As New Task
Dim db As New ProjectContext
GridView = (From t In db.Tasks Where t.AssignedTo = username Select t).ToList
End Sub
End Class
Here is my WPF Code where I want to bind the datagrid control to my GridView property in my TaskViewModel class.
Code:
x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProjectManagement"
mc:Ignorable="d"
Title="Tasks" Height="350" Width="525" Loaded="Window_Loaded">
<Window.DataContext>
<local:TaskViewModel/>
</Window.DataContext>
<Grid>
<DataGrid x:Name="dgTasks" ItemsSource="{Binding GridView}" AutoGenerateColumns="True" HorizontalAlignment="Left" Height="150" Margin="160,40,0,0" VerticalAlignment="Top" Width="347" >
</DataGrid>
</Grid>
</Window>
If I do a MessageBox.Show(GridView.Item(0).AssignedTo.ToString) after I set the GridView property, it pulls data out of that list field...so I know my Gridview is loaded. So I am just screwing up the WPF binding somehow.
Any guidance would be appreciated.
-
May 15th, 2017, 12:15 PM
#2
Re: WPF Having Trouble with binding a Datagrid control on load.
There's a lot of things that could be happening. I don't feel like I can see all of your code, so I don't feel like I can tell you exactly what things to look at.
For example, your names are very strange. This may seem like a strange place to start, but having confusing names makes working through code more confusing. For example, "Task" is also a .NET type for working with background threads. I'd name it "CustomerTask" or something specific. Also, you named a property "GridView" but it's a list of "Task". I would name this property "CustomerTasks".
There's some questions that need answers, though.
Who calls CallTaskData(), if anyone at all? What is ProjectContext? What does ProjectContext.Tasks contain, and how does it get those things? I need to know all of those things to understand what might be in the GridView property.
Also: for data binding to work and update properly, all properties involved have to either be Dependency Properties or implement the INotifyPropertyChanged pattern. Your TaskViewModel does not implement INotifyPropertyChanged, and thus your GridView property doesn't have a way to tell WPF when it changes. So it probably does change, but since it doesn't tell the UI the UI doesn't update.
INotifyPropertyChanged is easy to implement, it makes you declare an event named PropertyChanged that is raised whenever a property changes. Every ViewModel should implement INotifyPropertyChanged, because it's rare you don't want to use them with data binding. This may sound intimidating if you're new, but implementing INotifyPropertyChanged is fairly easy. By the second or third time you do it, you'll have memorized it. (Many MVVM frameworks include a ViewModelBase that already implements it for you. For now, I suggest you stick to implementing it yourself.)
Unfortunately, that's not ENOUGH to fully implement data binding with collections. INotifyPropertyChanged will tell the UI "I have replaced my list with a new one", but it won't pass along, "An item has been added" or other collection-based messages. There's an INotifyCollectionChanged interface to deal with that, but MS has provided the class ObservableCollection(Of T) that behaves like a list and already implements it for you! So the GridView property should be ObservableCollection(Of T) instead of List(Of T).
Your TaskViewModel, without the renames I suggested, should look like this:
Code:
Public Class TaskViewModel
Implements INotifyPropertyChanged
Private _gridView As ObservableCollection(Of Task)
Public Property GridView As ObservableCollection(Of Task)
Get
Return _gridView
End Get
Set
_gridView = value
RaisePropertyChanged("GridView")
End Set
End Property
Public Sub CallTaskData(ByVal username As String)
...
End Sub
Private Sub RaisePropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
End Class
You said you're new, so I'll try and explain it.
First, you may get errors that can be solved by putting "Imports System.ComponentModel" at the top of your file. If you get other errors, describe them and post EXACTLY what is in your file and we can work through it.
An "interface" is sort of like a class that doesn't have any code in it. It's a list of Subs, Functions, Properties, and Events but no actual code.
When a class "implements an interface" (by adding the "Implements" line), it is saying, "I have code to implement every Sub, Function, Property, and Event in the interface I am naming." You can see this at the bottom, where I declare the event from INotifyPropertyChanged AND I have to add an "Implements" line after it. (This is because VB, being a difficult language, lets you name your implementing things differently than what they are in the original interface, and even if you use the same name you have to type more words.)
To raise an event involves calling RaiseEvent and creating the appropriate data for the event. This is tedious enough people usually write some kind of "RaiseXXX" method to do it for them. That's why I wrote RaisePropertyChanged(), it needs to be called any time a property in TaskViewModel changes with the name of the property that changed.
Finally, I had to update the property to raise the event when it changes. There is no automatic syntax for this. I've been thinking for at least 8 years there should be one, but it hasn't made the cut yet.
If that's not working, I say "simplify things". I don't know how to make one of your task classes, but I'd temporarily replace CallTaskData() with this:
Code:
Public Sub CallTaskData(ByVal username As String)
GridView = New ObservableCollection(Of Task)()
GridView.Add(New Task())
End Sub
If creating a new collection and adding one item doesn't work, you know the problem is with your binding infrastructure. If it does work, you know the problem is with the lines that you removed to make way for this simple method. That's a debugging trick a lot of people don't learn for a long time: when things aren't working, replace complex code with very simple code. If the simple code doesn't work, there's no point looking at the complex version because the problem is further up the chain.
So since you're new, it may help to get in the habit of STARTING with very simple code like that last sample above. It looks stupid, and it doesn't do anything. But if it doesn't work, there's no point trying something more complex, you have to figure out why it's not working. New programmers tend to try and write the entire program, then push "Run" only to find it's failing. Then they have to go over EVERY line of code to try and find the problem. I've been writing code for 10+ years, and I tend to run my program every 3-10 lines I write to see if those lines work. If they don't, I only have to look at 3-10 lines for the problem. If they do, I can keep going, knowing that there are no problems yet.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
May 15th, 2017, 01:31 PM
#3
Thread Starter
Lively Member
Re: WPF Having Trouble with binding a Datagrid control on load.
I see... Well Task is the name of one of the database tables so the entity framework autogenerated a Task class from that. ProjectContext is the datacontext of the project database that the task table is in.
Code:
Partial Public Class Task
Public Property TaskId As Integer
Public Property AssignedBy As String
Public Property AssignedTo As String
Public Property DueDate As Nullable(Of Date)
Public Property Type As String
Public Property Priority As String
Public Property Notes As String
Public Property ProjectProjectId As Integer
Public Property Username As String
Public Overridable Property Project As Project
Public Sub New()
End Sub
End Class
I just have a window_loaded event that that instantiates TaskViewmodel and calls the CallTaskData method and passes the username of the machine.
Code:
Class MainWindow
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
Dim initTaskdata As New TaskViewModel
initTaskdata.GetUserTaskData(Environment.UserName)
End Sub
End Class
I implemented the code you showed in my TaskModel. Unfortuantely I still get just a blank datagrid with a blank header row.
Code:
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Public Class TaskViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private _gridview As ObservableCollection(Of Task)
Public Sub CallTaskData(username As String)
GridView = New ObservableCollection(Of Task)()
GridView.Add(New Task())
End Sub
Public Property GridView As ObservableCollection(Of Task)
Get
Return _gridview
End Get
Set(value As ObservableCollection(Of Task))
_gridview = value
RaisePropertyChanged("GridView")
End Set
End Property
Private Sub RaisePropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
So I think I am binding incorrectly.
-
May 16th, 2017, 12:12 PM
#4
Re: WPF Having Trouble with binding a Datagrid control on load.
Your code in CallTaskData method does not populate the GridView with a filtered record from the database, it only adds a new instance of a Task object. That could be the issue.
- kgc
-
May 16th, 2017, 01:44 PM
#5
Re: WPF Having Trouble with binding a Datagrid control on load.
Originally Posted by KGComputers
Your code in CallTaskData method does not populate the GridView with a filtered record from the database, it only adds a new instance of a Task object. That could be the issue.
- kgc
Yes, we're well aware, I buried this at the bottom when I asked for this change:
If creating a new collection and adding one item doesn't work, you know the problem is with your binding infrastructure. If it does work, you know the problem is with the lines that you removed to make way for this simple method.
leadhead: I had a big fat example prepared, then in my final conclusions I noticed the actual problem. Let's talk about it.
Here's the culprit:
Code:
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
Dim initTaskdata As New TaskViewModel
initTaskdata.GetUserTaskData(Environment.UserName)
End Sub
The "New" keyword there is the giveaway. When your XAML loads, you've set the Window.DataContext property to an instance of TaskViewModel. We'll call this 'Instance1'. Then your Window is displayed, and the Loaded event is raised. Your handler creates a new TaskViewModel we'll call 'Instance2'. Then Instance2's GetUserTaskData() is called. If you have breakpoints in place, you'll see it update its GridView property, but not the UI update. Why?
The Window is looking at Instance1. You need to call that instance's version of the method. But that's tough to do, since it's a resource in XAML, isn't it? This is an aspect of MVVM people argue about sometimes, "Where should I create my ViewModels? Who sets DataContext?" There's a lot of choices, each with its own benefits and costs. I like XAML-centric solutions, when I can design them.
But I can't really do a XAML-centric solution without getting into some topics I think are out of the scope of what you need to get moving. The problem: XAML is only able to use bindings to ICommands to execute code, but MS did not implement a good way to connect an Event to a Command. You can add event handlers in XAML, but then you have to implement an event handler method in your code-behind, and it may not have access to the ViewModel in the way you want. Some people have created special constructs (usually named EventToCommand) that execute a command when an event is raised, but it'd take me a page of tutorial to get you there. So this is a real pickle.
So we're going to have to do some things in code-behind. Open MainWindow.Xaml, and get into the Window tag. If you type "Loaded=", Intellisense ought to suggest <new event handler>, take that. The end result should look like this:
Code:
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Loaded="Window_Loaded"
In your code-behind, this probably added the following code:
Code:
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
End Sub
If it didn't, well, we're about to do some pasting, so just paste this in:
Code:
Class MainWindow
Private _viewModel As MyDataViewModel
Public Sub New()
InitializeComponent()
_viewModel = CType(DataContext, MyDataViewModel)
End Sub
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
_viewModel.CallTaskData("test")
End Sub
End Class
What's happening here? Let's walk through it.
The XAML changes we made say, "When the Loaded event is raised, please call Window_Loaded() as the event handler."
The Sub New() in MainWindow first does some stuff to set up the UI. If you leave out the InitializeComponent() part, your XAML won't be loaded and a lot of other things won't get set up, so never forget it and don't write code above it. Your Window is not ready until it finishes!
The next line converts the DataContext to the right type and stores it in a private variable so we can get to it later. Some people argue this is bad, because it encourages you to write code-behind instead of using XAML. When you're more experienced, you can worry about this aspect of "being good".
Note that since I'm assigning a variable and not using the "New" keyword, this means the private variable references the same instance as the DataContext property. That is to say, both variables point at "Instance1" from earlier. This is what we want!
Finally, the event handler calls the appropriate method on the ViewModel. This should update the right collection and get you some results. Verify it with our test code, then if that works, you can try putting the database-oriented code back in.
I'm pretty sure that will work.
Write yourself a note and remember you should someday ask, "But how would I do this without code-behind?"
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
May 17th, 2017, 02:39 PM
#6
Thread Starter
Lively Member
Re: WPF Having Trouble with binding a Datagrid control on load.
YESSSSSS!!!!! It works! How can I mail you beer or something?
Once I straightened out my MainWindow as you instructed, it worked exactly as I wanted it to! I am totally fine with a small amount of code in my view for now.
One other quick question. I am confused about what code goes in the model and what code goes in the viewmodel.
I started writing database query stuff in the model and then calling it from the viewmodel. But I am also new to entity framework and when I changed my "designer first" database, it blew all my code away and just regenerated the autogenerated properties that reference the database table fields.
Will it completely screw me up down the road if I just write all my code in the viewmodel? I suppose the part of the answer to that is "it depends" on what I am doing.
Thank you sooooo much for the assistance!
-
May 18th, 2017, 10:44 AM
#7
Re: WPF Having Trouble with binding a Datagrid control on load.
Originally Posted by leadhead
One other quick question. I am confused about what code goes in the model and what code goes in the viewmodel.
I started writing database query stuff in the model and then calling it from the viewmodel. But I am also new to entity framework and when I changed my "designer first" database, it blew all my code away and just regenerated the autogenerated properties that reference the database table fields.
Will it completely screw me up down the road if I just write all my code in the viewmodel? I suppose the part of the answer to that is "it depends" on what I am doing.
I have a hard time writing short answers to this kind of question, the longer answer is fun but it's really hard to explain.
Model types are very low abstraction. They tend to interact with the file system, databases, etc. The Model "layer" also tends to be subdivided into many layers, with the "top" being the only part you'd like the ViewModel layer to see.
ViewModel types are "in the middle" in terms of abstraction. Their job is to manipulate the lower-level Model concepts into something the higher-level Views can work with. This is important so you don't have to make choices that make the Model types clunky just to make the View less clunky. I call this layer "glue" sometimes, and it's where some really ugly code can live.
View types can only talk in very high-level abstractions, and ideally you don't really want them to know much about their ViewModel. Views just kind of say, "I need something with this set of properties."
Entity Framework operates at a somewhat high level of the Model. Its job is to deal with the low-level details of talking to the database and expose them as higher-level concepts like "collections of objects". But to work with EF is to be somewhat aware a database is involved, and I don't like my ViewModels to think like that. I want them to talk to "a thing that can fetch other things" so I have the freedom to change how I use EF, or that I use EF, without having to update VMs.
That's why we separate the application into "layers", it's about isolating change.
I *could* choose to let my VMs know about the database and interact with EF directly. It might make my application easier to write. But if I ever do want to change to something that isn't EF, that makes it more tricky. On the other hand, if you know up-front you're never going to use anything but EF, it might be a way to reduce your application's complexity.
That's the tradeoff with isolation: it makes the application more complex, so we only use it when we think the consequences/likelihood of change justify it.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
May 18th, 2017, 12:12 PM
#8
Thread Starter
Lively Member
Re: WPF Having Trouble with binding a Datagrid control on load.
That explains it perfectly! Thanks for your time!
Tags for this Thread
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|