The right way to invalidate view model on time - c#

After a bit of research, I haven't found the solution to invalidate and reload view models data on time.
Imagine an application with view model, which is filled with data from the server when initialized, and after some time (let's say, 1 minute), this data gets invalidated and reloaded from the server again.
As I assume, creating dispatcher timer in the view model itself isn't a good idea, because that doesn't feel right to me that view model should be responsible for that kind of things.
In this video author demonstrates creating dispatcher timer in the view model to update some data after a period of time. That's VB.NET, but it looks very similarly to what I'm trying to achieve.
As I see that now, using the dispatcher in the view model:
// ...
public class Foo
{
public string Bar { get; set; }
public int Baz { get; set; }
// ...
}
// ...
public interface IDataService
{
Task<Foo> GetDataAsync();
}
public class FooViewModel : INotifyPropertyChanged
{
private string _bar;
public string Bar
{
get
{
return _bar;
}
set
{
_bar = value;
OnPropertyChanged(nameof(Bar));
}
}
private int _baz;
public int Baz
{
get
{
return _baz;
}
set
{
_baz = value;
OnPropertyChanged(nameof(Baz));
}
}
IDataService service;
DispatcherTimer dispatcherTimer;
private async void dispatcherTimer_Tick(object sender, EventArgs eventArgs)
{
await InvalidateAndReloadDataAsync();
}
private void InitializeDispatcher()
{
dispatcherTimer = new DispatcherTimer();
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
dispatcherTimer.Interval = new TimeSpan(0, 1, 0);
dispatcherTimer.Start();
}
public async Task InvalidateAndReloadDataAsync()
{
Foo foo = await service.GetDataAsync();
Bar = foo.Bar;
Baz = foo.Baz;
}
public FooViewModel(IDataService dataService)
{
service = dataService;
InitializeDispatcher();
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName()] string name = null)
{
if (name != null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
// ...
}
Is it normal to actually use the dispatcher in the view model or there is much better solution?

Related

c# MVVM InvalidOperationException on BackgroundWorker when using Dispatcher

i'm currently facing an issue in C# WPF. I wrote an application, that generates long running reports in a background task. I am using prism with MVVM and trying to run the expensive background task with a Async ICommand implementation and a BackgroundWorker. But when i try to retrieve the resulting report
Report = asyncTask.Result;
i get an InvalidOperationException stating "The calling thread cannot access this object because a different thread owns it.".
Yes, i have already tried to invoke a dispatcher (its the first thing you'll find on google, stackoverflow etc when you search for the exception message). I have tried several variants like for instance:
Dispatcher.CurrentDispatcher.Invoke(() => Report = asyncTaks.Result);
or
Report.Dispatcher.Invoke(() => Report = asyncTask.Result);
but each time i get this exception.
I am suspecting that the way i am calling the report UI is not adequate.
The structure looks in brief as follows:
MainWindowViewModel
-> SubWindowCommand
SubWindowViewModel
-> GenerateReportCommand
ReportViewModel
-> GenerateReportAsyncCommand
<- Exception on callback
I am out of ideas, does anybody have a clue what i might be doing wrong?
Below are a few code fragments
Report Generator View Model:
public class ReportFlowDocumentViewModel : BindableBase
{
private IUnityContainer _container;
private bool _isReportGenerationInProgress;
private FlowDocument _report;
public FlowDocument Report
{
get { return _report; }
set
{
if (object.Equals(_report, value) == false)
{
SetProperty(ref _report, value);
}
}
}
public bool IsReportGenerationInProgress
{
get { return _isReportGenerationInProgress; }
set
{
if (_isReportGenerationInProgress != value)
{
SetProperty(ref _isReportGenerationInProgress, value);
}
}
}
public ReportFlowDocumentView View { get; set; }
public DelegateCommand PrintCommand { get; set; }
public AsyncCommand GenerateReportCommand { get; set; }
public ReportFlowDocumentViewModel(ReportFlowDocumentView view, IUnityContainer c)
{
_container = c;
view.DataContext = this;
View = view;
view.ViewModel = this;
InitializeGenerateReportAsyncCommand();
IsReportGenerationInProgress = false;
}
private void InitializeGenerateReportAsyncCommand()
{
GenerateReportCommand = new CreateReportAsyncCommand(_container);
GenerateReportCommand.RunWorkerStarting += (sender, args) =>
{
IsReportGenerationInProgress = true;
var reportGeneratorService = new ReportGeneratorService();
_container.RegisterInstance<ReportGeneratorService>(reportGeneratorService);
};
GenerateReportCommand.RunWorkerCompleted += (sender, args) =>
{
IsReportGenerationInProgress = false;
var report = GenerateReportCommand.Result as FlowDocument;
var dispatcher = Application.Current.MainWindow.Dispatcher;
try
{
dispatcher.VerifyAccess();
if (Report == null)
{
Report = new FlowDocument();
}
Dispatcher.CurrentDispatcher.Invoke(() =>
{
Report = report;
});
}
catch (InvalidOperationException inex)
{
// here goes my exception
}
};
}
public void TriggerReportGeneration()
{
GenerateReportCommand.Execute(null);
}
}
This is how i start the ReportView Window
var reportViewModel = _container.Resolve<ReportFlowDocumentViewModel>();
View.ReportViewerWindowAction.WindowContent = reportViewModel.View;
reportViewModel.TriggerReportGeneration();
var popupNotification = new Notification()
{
Title = "Report Viewer",
};
ShowReportViewerRequest.Raise(popupNotification);
with
ShowReportViewerRequest = new InteractionRequest<INotification>();
AsyncCommand definition
public abstract class AsyncCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public event EventHandler RunWorkerStarting;
public event RunWorkerCompletedEventHandler RunWorkerCompleted;
public abstract object Result { get; protected set; }
private bool _isExecuting;
public bool IsExecuting
{
get { return _isExecuting; }
private set
{
_isExecuting = value;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
protected abstract void OnExecute(object parameter);
public void Execute(object parameter)
{
try
{
onRunWorkerStarting();
var worker = new BackgroundWorker();
worker.DoWork += ((sender, e) => OnExecute(e.Argument));
worker.RunWorkerCompleted += ((sender, e) => onRunWorkerCompleted(e));
worker.RunWorkerAsync(parameter);
}
catch (Exception ex)
{
onRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
}
}
private void onRunWorkerStarting()
{
IsExecuting = true;
if (RunWorkerStarting != null)
RunWorkerStarting(this, EventArgs.Empty);
}
private void onRunWorkerCompleted(RunWorkerCompletedEventArgs e)
{
IsExecuting = false;
if (RunWorkerCompleted != null)
RunWorkerCompleted(this, e);
}
public virtual bool CanExecute(object parameter)
{
return !IsExecuting;
}
}
CreateReportAsyncCommand:
public class CreateReportAsyncCommand : AsyncCommand
{
private IUnityContainer _container;
public CreateReportAsyncCommand(IUnityContainer container)
{
_container = container;
}
public override object Result { get; protected set; }
protected override void OnExecute(object parameter)
{
var reportGeneratorService = _container.Resolve<ReportGeneratorService>();
Result = reportGeneratorService?.GenerateReport();
}
}
I think i understand my problem now. I cannot use FlowDocument in a BackgroundThread and update it afterwards, right?
So how can i create a FlowDocument within a background thread, or at least generate the document asynchronously?
The FlowDocument i am creating contains a lot of tables and when i run the report generation synchronously, the UI freezes for about 30seconds, which is unacceptable for regular use.
EDIT:
Found the Solution here:
Creating FlowDocument on BackgroundWorker thread
In brief: I create a flow document within my ReportGeneratorService and then i serialize the FlowDocument to string. In my background worker callback i receive the serialized string and deserialize it - both with XamlWriter and XmlReader as shown here
Your Problem is that you create FlowDocument in another thread. Put your data to the non GUI container and use them after bg comes back in UI thread.

Refresh or update content page every few seconds automatically

I am using Xamarin.forms (PCL) and I need to refresh/update Content Page with its data every few seconds. The data is retrieved from API in the viewmodel.
Is there any method or handler that can be used periodically to call the Get Api periodically inside the page.xaml.cs, something like:
methodRunPeriodically()
{
userdata = await UserService.GetUserasync(_UserViewModel.EmployeeId);
}
Xamarin.Forms has an API for starting a timer that you might find useful for this, documented here.
Device.StartTimer (TimeSpan.FromSeconds(10), () => {
// If you want to update UI, make sure its on the on the main thread.
// Otherwise, you can remove the BeginInvokeOnMainThread
Device.BeginInvokeOnMainThread(() => methodRunPeriodically());
return shouldRunAgain;
});
Based on the code in the above question, you would ensure that:
Your userdata object implements IPropertyChange as follows:
//Other usings skipped for brevity
...
...
using System.ComponentModel;
using System.Runtime.CompilerServices;
// This is a simple user class that
// implements the IPropertyChange interface.
public class DemoUser : INotifyPropertyChanged
{
// These fields hold the values for the public properties.
private string userName = string.Empty;
private string phoneNumber = string.Empty;
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public DemoUser()
{
}
public string Id { get; set; }
public string UserName
{
get
{
return this.userName;
}
set
{
if (value != this.userName)
{
this.userName = value;
NotifyPropertyChanged();
}
}
}
public string PhoneNumber
{
get
{
return this.phoneNumber;
}
set
{
if (value != this.phoneNumber)
{
this.phoneNumber = value;
NotifyPropertyChanged();
}
}
}
}
In your ContentPage, you then try the following, (I slightly modified the code by others above):
public class UserPage : ContentPage
{
private DemoUser demoUser;
private int intervalInSeconds;
public UserPage()
{
//Assuming this is a XAML Page....
InitializeComponent();
}
public UserPage(DemoUser demoUser, int intervalInSeconds = 10) : this()
{
this.demoUser = demoUser;
this.intervalInSeconds = intervalInSeconds;
this.BindingContext = this.demoUser;
Device.StartTimer(TimeSpan.FromSeconds(this.intervalInSeconds), () =>
{
Device.BeginInvokeOnMainThread(() => refreshDemoUser());
return true;
});
}
private async void refreshDemoUser()
{
this.demoUser = await getDemoUserById(this.demoUser.Id);
}
}
You can do as follows to run a Task when 10 seconds has passed. Returning true in Device.StartTimer will ensure that the Timer keeps running. Also, you want to ensure that you invoke the method on the main thread to update the UI:
public MyConstructor()
{
StartTimer();
}
private void StartTimer()
{
Device.StartTimer(System.TimeSpan.FromSeconds(10), () =>
{
Device.BeginInvokeOnMainThread(UpdateUserDataAsync);
return true;
});
}
private async void UpdateUserDataAsync()
{
userdata = await UserService.GetUserasync(_UserViewModel.EmployeeId);
}
If your API doesn't expose an EventHandler that you can subscribe to, then you need to do as mentioned in my example above.
You should just bind the UI to properties in your ViewModel and then set those properties appropriately. Calling OnPropertyChanged() will trigger Xamarin.Forms to update the UI based on the bound properties. Something like below:
//Code in Page
public class MyPage : ContentPage
{
public MyPage()
{
var entry = new Entry();
BindingContext = new MyViewModel();
entry.SetBinding<MyViewModel>(Entry.TextProperty, vm=>vm.EntryText);
Content = entry;
}
}
//Code in ViewModel
public class MyViewModel() : INotifyPropertyChanged
{
public MyViewModel()
{
Task.Factory.StartNew(()=> methodRunPeriodically());
}
string entryText;
public string EntryText
{
get { return entryText; }
set
{
if(entryText == value)
return;
entryText = value;
OnPropertyChanged();
}
}
bool shouldRun = true;
async Task methodRunPeriodically()
{
while(shouldRun)
{
userdata = await UserService.GetUserasync(_UserViewModel.EmployeeId);
EntryText = userdata.FirstName;
await Task.Delay(5000); //Run this every 5 seconds
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In this pattern, we are kicking off a long-running task that will run in a loop. It is reaching out to refresh the userData every 5 seconds and then setting the EntryText property. In the setter of the EntryText property in our ViewModel, we are calling OnPropertyChanged() which will cause Xamarin.Forms to update the UI. Calling OnPropertyChanged() triggers Xamarin.Forms to switch thread context from the background task to the UI thread and then back to the background task.
I didn't write this in XAML, but the binding would be pretty much the same except the entry would be like below:
<Entry Text={Binding EntryText}/>
EDIT
#therealjohn's answer is good also. You could use that instead of my while loop like below:
bool shouldRun = true;
methodRunPeriodically()
{
Device.StartTimer(TimeSpan.FromSeconds(5), () =>
{
userdata = await UserService.GetUserasync(_UserViewModel.EmployeeId);
EntryText = userdata.FirstName;
return shouldRun;
});
}
You can review what the Forms source code is doing with the Device.StartTimer on the native iOS and Android.
Update UI every one second:
Device.StartTimer(TimeSpan.FromMilliseconds(1000), loop2);
bool loop2()
{
Device.BeginInvokeOnMainThread(() => updateUI());
return true;
}
or:
Device.StartTimer(TimeSpan.FromMilliseconds(1000), loop2);
bool loop2()
{
Device.BeginInvokeOnMainThread(() => {
updateUI();
//more stuff;
});
return true;
}

Backgroundworker exits after first expression

I have a list view bound to an ObservableCollection. With that I want to mock a chat application with a WPF gui.
To simulate some activity I wanted to use a Background worker who spams a little bit. But the worker always exits his loop after executing the first statment, so my question is: why does he do that and how to fix it?
here is the code so far:
public partial class MainWindow : Window, INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private string pCurrentUsername;
public string currentUsername
{
get { return pCurrentUsername; }
set
{
pCurrentUsername = value;
if (null != this.PropertyChanged)
{
PropertyChanged(this, new PropertyChangedEventArgs("currentUsername"));
}
}
}
ObservableCollection<ChatPost> items = new ObservableCollection<ChatPost>();
BackgroundWorker bgWorker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
currentUsername = "Me";
items.Add(new ChatPost("this", "that"));
bgWorker.DoWork += new DoWorkEventHandler(mockBussiness);
bgWorker.RunWorkerAsync();
lvChat.ItemsSource = items;
}
private void mockBusiness(object o, DoWorkEventArgs args)
{
while (!bgWorker.CancellationPending)
{
items.Add(new ChatPost("guy1", "Ey man!"));
items.Add(new ChatPost("guy2", "What man?"));
}
}
private void btSend_Click(object sender, RoutedEventArgs e)
{
items.Add(new ChatPost(currentUsername, tbMessage.Text));
}
}
public class ChatPost
{
public ChatPost()
{ }
public ChatPost(string username, string message)
{
this.username = username;
this.message = message;
}
public string username { get; set; }
public string message { get; set; }
}
So the only thing that gets executed (meaning printed) is one time "Ey man!"
Yes, you're modifying the UI (indirectly, through the ObservableCollection<>) on a non-UI thread. You're not allowed to do that. I suspect you should find an exception being thrown giving that detail, although it may not be easy to find.
You need to marshal back to the UI thread for any threading operations, in general. If you're using WPF on .NET 4.5, apparently you can using BindingOperations.EnableCollectionSynchronization for this, but I admit I have no direct experience of this.

C# strange Double ObservableCollection behaviour

Maybe I don't understand the ObservableCollection well enough. But as far as I knew it was similar to a normal list, but with event triggers so that you can react to changes.
So I have this Windows store app. And in this application I have a main BusinessModel class which is the main source for all data in my client application. This data will be updated when the server has made some changes elsewhere. In the future I'd like to have this class update the ViewModels for specific data updates etc.
So I also have a ViewModel class which contains, at least in my PoC's so far, a copy of that list (also in the near future this list will have an enriched version of the list).
Since it's a copy they should be both separate instances and have their own separate items.
However when I update the copy in the ViewModel, the BusinessModel version changes with it.
And vice versa.
I can't seem to figure out why this is happening. Underneath you will find the classes and their functions:
//the BusinessModel Class
public class ModelStuff : INotifyPropertyChanged
{
private ObservableCollection<DataObject> _modelStuff;
public ObservableCollection<DataObject> modelStuff
{
get
{
return _modelStuff;
}
set
{
_modelStuff = value;
NotifyPropertyChanged("modelStuff");
}
}
private static ModelStuff businessModel;
public static ModelStuff BusinessModel
{
get
{
if (businessModel == null)
{
businessModel = new ModelStuff();
}
return businessModel;
}
}
public ModelStuff()
{
modelStuff = new ObservableCollection<DataObject>();
modelStuff.Add(new DataObject(0));
modelStuff.Add(new DataObject(1));
modelStuff.Add(new DataObject(2));
modelStuff.Add(new DataObject(3));
modelStuff.Add(new DataObject(4));
modelStuff.Add(new DataObject(5));
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
//the ViewModel class
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<DataObject> _visibleStuff;
public ObservableCollection<DataObject> visibleStuff
{
get
{
return _visibleStuff;
}
set
{
_visibleStuff = value;
NotifyPropertyChanged("visibleStuff");
}
}
private static ViewModel tvm;
public static ViewModel TVM
{
get
{
if (tvm == null)
{
tvm = new ViewModel();
}
return tvm;
}
}
public ViewModel()
{
visibleStuff = new ObservableCollection<DataObject>(ModelStuff.BusinessModel.modelStuff.OrderBy(c => c.testNumber));
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
//the TestObjects
public class DataObject
{
public int testNumber { get; set; }
public String testStr { get; set; }
public DataObject(int i)
{
testNumber = i;
testStr = "testje";
}
}
//A randomly placed button invokes this function when clicked.
private void Button_Click(object sender, RoutedEventArgs e)
{
//do stuff here
int i0 = ModelStuff.BusinessModel.modelStuff[0].testNumber;
ViewModel.TVM.visibleStuff[0].testNumber = 100;
int i1 = ModelStuff.BusinessModel.modelStuff[0].testNumber;
//i1 has the value 100 in my logs! :S
}
//Second version but vice versa
private void Button_Click(object sender, RoutedEventArgs e)
{
//do stuff here
int i0 = ViewModel.TVM.visibleStuff[0].testNumber;
ModelStuff.BusinessModel.modelStuff[0].testNumber = 100;
int i1 = ViewModel.TVM.visibleStuff[0].testNumber;
//i1 has the value 100 in my logs! :S
}
Where has my reasoning gone wrong?
Why is this happening?
And more importantly, how can I prevent this behaviour?
As far as I can see, your line of code:
visibleStuff = new ObservableCollection<DataObject>(ModelStuff.BusinessModel.modelStuff.OrderBy(c => c.testNumber));
is not making a copy of the underlying objects at all. It is adding the same DataObjects from the original list to a new ObservableCollection.
You need to clone the DataObjects individually and add them to the new collection. Something like this should do it:
visibleStuff = new ObservableCollection<DataObject>(ModelStuff.BusinessModel.modelStuff.OrderBy(c => c.testNumber).Select(i => new DataObject(i.testNumber)));

MonoTouch Exception: Selector invoked from objective-c on a managed object of type that has been GC'ed

I'm getting this exception in my MonoTouch app and I can't seem to fix it. I've been trying for about 6 hours now and I'm having no luck.
My understanding of this exception is that an object is being referenced (or trying to be referenced) by MonoTouch but Garbage Collection has already disposed of it. Therefore it is looking to build a reference to it again using a constructor which I haven't set in the class, using a pointer.
Originally I thought that adding that constructor would be enough until I did some research and realised that at best, it would be a temporary bandage. What I'm finding strange is that as far as I can see I am holding a reference to everything. I've even gone a bit overboard and started creating member variables for things that don't necessarily need to be kept to try and capture the exception, but still nothing.
The error occurs when I load a ViewController, trigger a delegate which pushes a new ViewController onto the NavigationController (thus leaving the screen), hitting the back button and moving onto another view again. This consistently happens the same way and will always crash at this point, though not necessarily at the exact same time due to the way GC works I suppose.
The app is quite simple so far. The first screen loads 5 different charts using ShinobiCharts. Data Sources and Delegates are set in the ViewController and a ViewModel is sent to the View which then adds the charts to the subview. The exception occurs in ColumnChart (a custom made class inheriting from ShinobiChart). The exception will always be thrown on these controls never the View or the ViewController. However, which chart causes the exception is seemingly random every time.
How I'm storing the charts looks like this:
public class HomeViewModel
{
public ColumnChart MeetingsChart { get; set; }
public ColumnChart FirmDataChart { get; set; }
public ColumnChart SystemUseChart { get; set; }
public BarChart IllustrationsChart { get; set; }
public PieChart TermsOfBusinessChart { get; set; }
public ColumnChartDataSource MeetingsChartDataSource { get; set; }
public ColumnChartDataSource SystemUseChartDataSource { get; set; }
public StackChartDataSource FirmDataChartDataSource { get; set; }
public BarChartDataSource IllustrationsChartDataSource { get; set; }
public PieChartDataSource TermsOfBusinessDataSource { get; set; }
public BarChartDataProvider TermsOfBusinessDataProvider { get; set; }
public DashboardMeetingChartDelegate MeetingsChartDelegate { get; set; }
public ColumnChartDelegate SystemUseChartDelegate { get; set; }
public PieChartDelegate TermsOfBusinessDelegate { get; set; }
}
This class is then implemented in the Controller like this: (NOTE: Lots of code was stripped out as I didn't think it was needed)
public class HomeController : UIViewController
{
private HomeView _homeView;
private HomeViewModel _homeViewModel;
…..
public HomeController()
{
_homeViewModel = new HomeViewModel();
}
….
private void LoadCharts()
{
_homeViewModel.MeetingsChart = LoadMeetingsChart();
_homeViewModel.FirmDataChart = LoadFirmDataChart();
_homeViewModel.SystemUseChart = LoadSystemUseChart();
_homeViewModel.IllustrationsChart = LoadIllustrationsChart();
_homeViewModel.TermsOfBusinessChart = LoadTermsOfBusinessChart();
}
private ColumnChart LoadMeetingsChart()
{
ColumnChart meetingsChart = new ColumnChart(RectangleF.Empty);
meetingsChart.DataSource = _homeViewModel.MeetingsChartDataSource;
meetingsChart.Delegate = _homeViewModel.MeetingsChartDelegate;
meetingsChart.LicenseKey = LICENSE_KEY;
return meetingsChart;
}
}
Finally, the View:
public class HomeView : UIView
{
private HomeViewModel _homeViewModel;
public HomeView(RectangleF frame, UINavigationController navigationController, HomeViewModel homeViewModel)
: base(frame)
{
this._homeViewModel = homeViewModel;
this.BackgroundColor = UIColor.FromRGB(20, 20, 20);
this.Title = "Sales 360 Dashboard";
SetupChartBounds(UIApplication.SharedApplication.StatusBarOrientation);
}
public void SetupCharts()
{
SetupMeetingsChartHeaderBar();
SetupTermsOfBusinessChartHeaderBar();
SetupIllustrationsChartHeaderBar();
SetupSystemUserChartHeaderBar();
SetupFirmDataChartHeaderBar();
this.AddSubview(_homeViewModel.MeetingsChart);
this.AddSubview(_homeViewModel.TermsOfBusinessChart);
this.AddSubview(_homeViewModel.IllustrationsChart);
this.AddSubview(_homeViewModel.SystemUseChart);
this.AddSubview(_homeViewModel.FirmDataChart);
}
}
I'd really appreciate any help on this problem as I'm badly stuck on it. Thanks.
EDIT 1:
The HomeController is pushed from something called the SplashController. The only purpose of the SplashController is to display a splash screen view and call web services. The web services are called asynchronously and when completed will change the view to the home view where all charts are displayed.
Here is some of the SplashController class:
public class SplashController : UIViewController
{
private SplashView _splashView;
private SplashViewModel _splashViewModel;
private readonly string BACKGROUND_IMAGE_PATH;
private HomeController _homeController;
.....
public override void LoadView()
{
LoadViewModel();
_splashView = new SplashView(new RectangleF(0, 0, Dimensions.Width, Dimensions.Height), _splashViewModel);
this.View = _splashView;
}
// .... a number of service calls eventually leading to this
protected void GetTermsOfBusinessAllAgentsCompleted(object sender, GetTermsOfBusinessAllAgentsCompletedEventArgs e)
{
_servicesHelper.StopTimer(e.UserState as Timer);
if (e.Error != null)
HandleError(e.Error, "Terms of Business");
else
{
_termsOfBusinessGraphData = e.Result;
ChangeViewToHomeView();
}
}
private void ChangeViewToHomeView()
{
_homeController = new HomeController(_meetingsGraphData, _firmDataGraphData, _systemUseGraphData,
_illustrationsGraphData, _termsOfBusinessGraphData);
this.NavigationController.PushViewController(_homeController, false);
}
}
The HomeView is created in the LoadView method overridden in the HomeController class and basically looks like this:
_homeView = new HomeView(new RectangleF(0, 0, Dimensions.Width, Dimensions.Height), this.NavigationController, _homeViewModel);
this.View = _homeView;
As for the NavigationController, good question. This was something I was doing earlier where I was passing a NavigationController to a delegate. However, now all of this is done in the HomeController so it is no longer needed. Sorry about that, my mistake! I have now taken this out and tried re-running the app, the same thing still happens. Please ignore this bit.
EDIT 2:
Here is the delegate for one of the charts. I've missed out the methods which handles errors as it's not needed. The _meetingServices.GetAgentsData method calls a web service in that method which then returns to the event in this object:
public class DashboardMeetingChartDelegate : SChartDelegate
{
private UINavigationController _navigationController;
private CategoryGraph _meetingsGraphData;
private MeetingServices _meetingServices;
private ServicesHelper _servicesHelper;
private int _currentIndex;
public DashboardMeetingChartDelegate(UINavigationController navigationController, CategoryGraph meetingsGraphData)
{
this._navigationController = navigationController;
this._meetingsGraphData = meetingsGraphData;
_meetingServices = new MeetingServices();
_servicesHelper = new ServicesHelper();
_currentIndex = -1;
}
protected override void OnToggledSelection (ShinobiChart chart, SChartDataPoint dataPoint, SChartSeries series, PointF pixelPoint)
{
_currentIndex = dataPoint.Index;
_meetingServices.GetAgentsData(GetAgentsCompleted);
}
protected void GetAgentsCompleted(object sender, GetAgentsCompletedEventArgs e)
{
_servicesHelper.StopTimer(e.UserState as Timer);
if (e.Error != null)
HandleError(e.Error);
else
{
int rsmId = Convert.ToInt32(_meetingsGraphData.Data[0].SeriesDataPoints[_currentIndex].PointMetaData);
AgentContract currentAgent = e.Result.Agents.Where(agent => agent.Id == rsmId).First();
_navigationController.PushViewController(new MeetingController(currentAgent), true);
}
}
And here is the MeetingController - pretty similar to the other one except there's less to load:
public class MeetingController : UIViewController
{
private AgentContract _currentAgent;
private RsmChartView _meetingView;
private RsmChartViewModel _meetingViewModel;
private MeetingTypeController _meetingTypeController;
private ColumnChartDataProvider _columnChartDataProvider;
public MeetingController(AgentContract currentAgent)
{
_currentAgent = currentAgent;
}
public override void LoadView ()
{
LoadColumnChartDataProvider();
LoadViewModel();
InitialiseView();
}
private void LoadColumnChartDataProvider()
{
_columnChartDataProvider = new ColumnChartDataProvider();
_columnChartDataProvider.XValueList.Add(new NSString("Phone"));
_columnChartDataProvider.XValueList.Add(new NSString("Demo"));
_columnChartDataProvider.XValueList.Add(new NSString("Fact Finding"));
_columnChartDataProvider.YValueList.Add(61);
_columnChartDataProvider.YValueList.Add(22);
_columnChartDataProvider.YValueList.Add(27);
}
private void LoadViewModel()
{
_meetingViewModel = new RsmChartViewModel();
_meetingViewModel.Chart = LoadMeetingChart();
_meetingViewModel.RsmContentModel = LoadRsmContentModel();
}
private RsmContentModel LoadRsmContentModel()
{
RsmContentModel model = new RsmContentModel();
model.Name = _currentAgent.Name;
return model;
}
private void InitialiseView()
{
_meetingView = new RsmChartView(new RectangleF(0, 0, Dimensions.Width, Dimensions.Height), _meetingViewModel);
this.View = _meetingView;
}
private ColumnChart LoadMeetingChart()
{
ColumnChart meetingChart = new ColumnChart(new RectangleF(10, 220, Dimensions.Width - 20, 540));
_meetingTypeController = new MeetingTypeController();
_meetingViewModel.ChartDataSource = new ColumnChartDataSource(_columnChartDataProvider);
_meetingViewModel.ChartDelegate = new ColumnChartDelegate(this.NavigationController, _meetingTypeController);
meetingChart.DataSource = _meetingViewModel.ChartDataSource;
meetingChart.Delegate = _meetingViewModel.ChartDelegate;
return meetingChart;
}
public override void ViewDidLoad()
{
_meetingView.Load();
}
}
This condition is often hard to debug. Some changes have been made to MonoTouch to reduce such occurrences.
To try them I suggest you to try 6.0.7 (on the beta channel) and report any outstanding issue on the mailing list (or a bug report).
More details can be found in the release notes (direct link).

Categories