Accessing Controls from a different class....
Alright I have 3 forms - The load up form "QoE" and two public classes. I also have an progress bar on the main form "QoE." Ive been able to access controls in other classes in numerous other projects, but for some reason this project is giving me fits. I have no idea why I can not control the progress bar from my class "Utils."
The classes kind of look like this:
Code:
Public Class QoE
End Class
Public Class Utils
public shared sub ProgressBar
Dim f1 as New QoE()
f1.ProgressBarMain.Increment(+1)
f1.ProgressPercent.Text = f1.ProgressBarMain.Value.ToString() & "%"
end sub
End Class
Public Class Tests
public shared sub DoWork
Utils.Progressbar()
End Sub
End Class
Now, I was always under the impression that I could simply just call the controls like this:
QoE.ProgressBarMain.Increment(+1)
I actually have other projects where i did this, but now I am getting the error - "reference to a non shared member requires an object reference."
So Ive been tinkering with the above code because I have seen other people do it around the net. I don't receive an error, but it is also not doing anything.
I have all the controls set to public of course, so why is this not working?
Thanks Guys
Edit* I went back through my old projects and I definitely have called form1 controls before using something like form1.listbox.add() from form2. I cant figure out what is different though between that project and this project.
Re: Accessing Controls from a different class....
Think about this. Let's say that you have a notepad. I want to write something on that notepad. I go out and get a new notepad and write something on it. Would you expect what I write to magically appear on the notepad in front of you? Of course not. In that case, if the user has a QoE form in front of them and you then create a new QoE form and change its ProgressBar, why would you expect that to change the ProgressBar of the QoE form in front of the user? Just because two things are the same type does not mean that changing one will change the other. It doesn't work that way for objects in real life and so it doesn't work that way for objects in OOP, which are modelled on real-life objects.
Now, when you use just the type name to refer to a form you are actually using the default instance. To learn what that means, I suggest that you follow the Blog link in my signature and check out my post on Default Form Instances. If it's the default instance of QoE that you displayed in the first place then updating the default instance will have the desired effect. Why you apparently can't use the default instance in this case I don't know. That error message indicates that the class name is being interpreted as the class rather than the default instance of the class. That would generally only happen if the form didn't have a parameterless constructor, but the code you posted indicates that it does.
If this information doesn't help you solve the issue, I'd be interested to see the project. Please ensure that you delete the bin and obj files before zipping the solution and attach the ZIP file to a post.
Re: Accessing Controls from a different class....
It might be just me, but personally I don't think classes have any business updating forms like that in the first place. If they want to notify a form about progress, fine, raise and event, let the form handle the event and update the progressbar from there.
Just my two cents.
-tg
Re: Accessing Controls from a different class....
How can I check to make sure QoE is the default instance? Is it Project Properties > Application > Startup Object?
I get what you are saying about the notepad, Ive been sitting at my desk since 9am pst reading and trying to figure out why this works in one project and not another.
I dont mind sending you the project, but it is pretty large (Im trying to include a progress bar because sometimes it takes 3-4 minutes to run through all the scripts). Do you have anything off the top of your head I can try?
Re: Accessing Controls from a different class....
Quote:
Originally Posted by
techgnome
It might be just me, but personally I don't think classes have any business updating forms like that in the first place. If they want to notify a form about progress, fine, raise and event, let the form handle the event and update the progressbar from there.
Just my two cents.
-tg
I concur. If you check out the Multiple Forms link in my signature, you'll see that part 3 says that only the form on which a control resides should access any controls on that form. It says that other forms should not access those controls directly but that goes for any class at all. If an object is doing work for which the progress will be displayed on a form, as you say, that object should raise an event to indicate that progress has changed and it's up to whoever is listening to do with that what they will. That makes such a class more useful because it can be used in various places. The form should know about the object doing the work but not the other way around.
Re: Accessing Controls from a different class....
Quote:
Originally Posted by
Zmcpherson
How can I check to make sure QoE is the default instance? Is it Project Properties > Application > Startup Object?
I get what you are saying about the notepad, Ive been sitting at my desk since 9am pst reading and trying to figure out why this works in one project and not another.
I dont mind sending you the project, but it is pretty large (Im trying to include a progress bar because sometimes it takes 3-4 minutes to run through all the scripts). Do you have anything off the top of your head I can try?
What version of VB are you using? Default instances have definitely existed since VB 2008 at least but I can't recall whether they were introduced in 2008 or 2005. In the unlikely event that you're using a really old version, that would be the reason.
Is Startup Object an option? If so then that means either you are using a really old version or you've turned the Application Framework off, because you can only select a Startup Form with the Application Framework enabled. Default instances still work without the Application Framework though. It's just that the startup form is always the default instance of its type if the Application Framework is enabled but it's up to you either way if you write your own Main method.
As tg and I said, ideally, you shouldn't be updating the ProgressBar from anywhere but in the form anyway. It's legal to access it from outside but it's not good practice.
Re: Accessing Controls from a different class....
The way that I had it initially was that the ProgressBar() was a public shared sub on QoE.vb (the initial form)
so...
Code:
public class QoE
public shared sub ProgressBar()
progressbarmain.increment(+1)
end sub
end class
public class tests
public shared sub dowork()
QoE.ProgressBar()
end sub
end class
but with this I keep getting the error "Cannot refer to an instance member of a class from within a shared method or shared member initializer without an explicit instance of the class."
If I take the shared out, I dont get an error, but I cant call on it from the class "tests"
Im currently trying to use RaiseEvents to get me there.
Re: Accessing Controls from a different class....
Well this is how I ended up solving it, let me know if there is an easier way...
Code:
Public Shared Event ProgressBarEvent(ByVal mynum As Integer)
Private Sub ProgressBar(ByVal mynum As Integer) Handles Me.ProgressBarEvent
ProgressBarMain.Value = mynum
ProgessLabel.Text = ProgressBarMain.Value.ToString
End Sub
Public Shared Sub ProgressEventRaiser(ByVal percent As Integer)
RaiseEvent ProgressBarEvent(percent)
End Sub
and then I call it using: QoE.ProgressEventRaiser(value)
Re: Accessing Controls from a different class....
Quote:
Originally Posted by
Zmcpherson
The way that I had it initially was that the ProgressBar() was a public shared sub on QoE.vb (the initial form)
so...
Code:
public class QoE
public shared sub ProgressBar()
progressbarmain.increment(+1)
end sub
end class
public class tests
public shared sub dowork()
QoE.ProgressBar()
end sub
end class
but with this I keep getting the error "Cannot refer to an instance member of a class from within a shared method or shared member initializer without an explicit instance of the class."
If I take the shared out, I dont get an error, but I cant call on it from the class "tests"
Im currently trying to use RaiseEvents to get me there.
The issue there is fairly clear. You are trying to affect an instance of QoE from within a Shared method of QoE but a Shared method doesn't know anything about any instance so of course it can't affect one. You need to start thinking about programming objects the same way you think about real-world objects. If this QoE form was a paper form that you had to fill out with a pen, would you be able to write something on one instance of that form if you didn't have that instance in your hand? Again, of course not, so how can you affect an instance of the form in your app if you don't have access to that instance?
Basically, if you did this the right way in the first place then you wouldn't have to try dodgy workarounds to make it work. Here's an example of an instance of a class doing work and notifying a form of progress via an event and the form then updating its own ProgressBar. The worker object doesn't even know that the form exists, which is how it should be.
vb.net Code:
Imports System.Threading
Public Class Worker
Public Property Progress As Integer
Public Event ProgressChanged As EventHandler
Public Sub DoWork()
For i = 1 To 100
Thread.Sleep(100) 'simulated work
Progress = i
RaiseEvent ProgressChanged(Me, EventArgs.Empty)
Next
End Sub
End Class
vb.net Code:
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim worker As New Worker
AddHandler worker.ProgressChanged, AddressOf Worker_ProgressChanged
worker.DoWork()
RemoveHandler worker.ProgressChanged, AddressOf Worker_ProgressChanged
MessageBox.Show("Work complete.")
End Sub
Private Sub Worker_ProgressChanged(sender As Object, e As EventArgs)
Dim worker = DirectCast(sender, Worker)
ProgressBar1.Value = worker.Progress
Refresh()
End Sub
End Class
That's a bit of a dodgy example because everything is happening on the UI thread so the form will freeze but it demonstrates the principle nonetheless. The Worker class does the work but knows nothing about who it's doing the work for. It simply does the work and tells the universe where it's up to. It's up to whoever wants to listen to do something about that progress as it changes. In this case, the form that created the Worker object listens to those notifications and updates its own ProgressBar accordingly.
Re: Accessing Controls from a different class....
Quote:
Originally Posted by
Zmcpherson
Well this is how I ended up solving it, let me know if there is an easier way...
Code:
Public Shared Event ProgressBarEvent(ByVal mynum As Integer)
Private Sub ProgressBar(ByVal mynum As Integer) Handles Me.ProgressBarEvent
ProgressBarMain.Value = mynum
ProgessLabel.Text = ProgressBarMain.Value.ToString
End Sub
Public Shared Sub ProgressEventRaiser(ByVal percent As Integer)
RaiseEvent ProgressBarEvent(percent)
End Sub
and then I call it using: QoE.ProgressEventRaiser(value)
That's horrible and still dodgy. Stop using Shared to try to get around doing things the right way. Also, as I have demonstrated after having said several times, it's the object doing the work that raises the event, not the form.
Re: Accessing Controls from a different class....
I should of said that the progress bar already has its own thread
how the progress bar is being updated is something like this (my project is huge already, so these snippets are rather small):
Code:
dim somefile = file.readalllines("C:\thisfile.txt") 'Im just writing this freehand
dim percent as integer = somefile.length / 100
for x as integer = 0 to somefile.length - 1
if x mod percent = 0 then QoE.EventRaiser(x/percent)
'do rest of work
next
So the background worker is a better option?
Re: Accessing Controls from a different class....
What do you mean by the progress bar has it's own thread?
Re: Accessing Controls from a different class....
um... yeah... not sure what's meant by that either... but... um... it doesn't have its own thread. It is on the same thread as all of the other UI stuff... which is the main thread. Creating an instance of something doesn't make it on its own thread, it just makes a new instance of it. Same thread. You have to either use a BGW or create a new thread and spin it up. If you're not doing that (and judging by your last question there, I'm guessing not) then you're in a single thread.
-tg
Re: Accessing Controls from a different class....
Quote:
Originally Posted by
techgnome
um... yeah... not sure what's meant by that either... but... um... it doesn't have its own thread. It is on the same thread as all of the other UI stuff... which is the main thread. Creating an instance of something doesn't make it on its own thread, it just makes a new instance of it. Same thread. You have to either use a BGW or create a new thread and spin it up. If you're not doing that (and judging by your last question there, I'm guessing not) then you're in a single thread.
-tg
I'm actually going to go all out and say the user creates a secondary thread to invoke back to the UI thread and calls this multi threading. I see this alot :/
Re: Accessing Controls from a different class....
Yeah, controls don't have threads; threads have controls. Controls are objects like any other and there is absolutely no restriction on accessing objects from any thread at all. The only issue is scope: whether you're using one thread or multiple, if you can see an object then you can access its members.
The issue with controls is that they have an affinity for the thread that their handle was created on and, while you can access them on other threads, if that access makes use of the handle, there is no guarantee that it will work. It may do something unexpected or it may do nothing. That's why VS prevents you accessing controls on threads other than the ones that created them by default. A .NET app built for Release actually can do so freely, but there's no guarantee that it will work. A .NET app built for Debug will throw an exception by default.
Now, in the case of your ProgressBar, its handle must have been created on the same thread as the form it is on, otherwise trying to add it to the form would have failed with a cross-threading exception. As such, it's the UI thread that owns the ProgressBar, just as the UI thread owns the form. What you think is a thread owned by the ProgressBar is no such thing. It's just a thread doing some work.
If you want to create a single extra worker thread in a WinForms app then the easiest way is to use a BackgroundWorker. It already has the multi-threading built in and it also has events that automatically cross the thread boundary back to the UI thread so there's no need to use delegates. You can create the BackgroundWorker in a form or you can do it in some other class. It still works the same way. You call RunWorkerAsync on the UI thread and the DoWork event handler is executed on a worker thread. You call ReportProgress on the worker thread and the ProgressChanged event handler is executed on the UI thread. The work finishes and the RunWorkerCompleted event handler is executed on the UI thread.
If you need multiple worker threads then you can create multiple BackgroundWorkers but that's unusual. In older versions of .NET, you could create Threads explicitly or use the ThreadPool but .NET 4.0 introduced the Tasks Parallel Library (TPL) that makes using threads in general a bit more like using a BackgroundWorker. As such, you can look into using the Task class and associated types in .NET 4.0 and later. VB 2012 also introduces the Await and Async keywords, which make use of the TPL under the hood.