How to implement busy indicator in application shell - c#

I am trying to implement the busy indicator from the WPF Extended Toolkit in my application's "shell." The goal is to implement the indicator in one place and then be able to set the IsBusy property from anywhere so that it can be initialized. Here is my shell:
<Window x:Class="Foundation.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Library.Controls.Views;assembly=Library"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
WindowStyle="None"
AllowsTransparency="False"
Name="ShellView"
FontFamily="Yu Gothic Light"
Background="{StaticResource AiWhiteBrush}">
<!--Region Outer Most Grid-->
<xctk:BusyIndicator IsBusy="{Binding IsBusy}">
<Grid x:Name="OuterGrid">
<!-- CONTENT HERE -->
</Grid>
</xctk:BusyIndicator>
<!--End Region-->
Then, my Shell's ViewModel looks like this:
using CashDrawer.Views;
using Library.BaseClasses;
using Library.StaticClasses;
using Microsoft.Practices.Prism.Commands;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using WpfPageTransitions;
namespace Foundation
{
public class ShellViewModel : ViewModelBase
{
#region constructor(s)
public ShellViewModel()
{
StateManager.IsBusyChange += new StateManager.IsBusyHandler(IsBusyEventAction);
}
#endregion constructor(s)
#region properties
private bool _IsBusy;
public bool IsBusy
{
get
{
return _IsBusy;
}
set
{
if (_IsBusy != value)
{
_IsBusy = value;
OnPropertyChanged("IsBusy");
}
}
}
#endregion properties
#region actions, functions, and methods
private void IsBusyEventAction(object sender, EventArgs e)
{
if (StateManager.IsBusy)
{
this.IsBusy = true;
}
else
{
this.IsBusy = false;
}
}
#endregion actions, functions, and methods
}
}
Last, I have created a static StateManager class:
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using WpfPageTransitions;
namespace Library.StaticClasses
{
public static class StateManager
{
private static bool _IsBusy;
public static bool IsBusy
{
get
{
return _IsBusy;
}
set
{
if (_IsBusy != value)
{
_IsBusy = value;
IsBusyChange(null, null);
}
}
}
public delegate void IsBusyHandler(object sender, EventArgs e);
public static event IsBusyHandler IsBusyChange;
}
}
The idea is that when the StateManager's IsBusy property is changed, it will fire an event that will change the IsBusy property in the ShellViewModel accordingly. The logic is working fine. However, the busy indicator isn't working as expected. Here is a code snippet from another view model that switches the IsBusy property:
private void SaveCommand_Action()
{
StateManager.IsBusy = true;
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
}
}
StateManager.IsBusy = false;
}
I am seeing some lag in the code, but I never see the busy indicator appear. I can remove StateManager.IsBusy = false; and the busy indicator will appear (and show indefinitely of course.) I have tried creating a longer delay between the IsBusy state changes and the indicator still doesn't appear. I have read multiple posts and articles trying to understand what may be going wrong but I am not seeing anything helpful. I am aware that the IsBusy indicator is happening on the UI thread, but I am changing the IsBusy states in the ViewModel which should not be on the UI thread. Any thoughts or suggestions?

Regarding my last comment.
You could change the statemanager to take in an action instead, so you pass in the method, and the state manager can setup the invoke etc
public static class StateManager
{
public static void Process(Action action) {
IsBusy = true;
Application.Current.Dispatcher.Invoke(action, System.Windows.Threading.DispatcherPriority.Background);
IsBusy = false;
}
private static bool _IsBusy;
public static bool IsBusy {
get {
return _IsBusy;
}
set {
if (_IsBusy != value) {
_IsBusy = value;
IsBusyChange(null, null);
}
}
}
public delegate void IsBusyHandler(object sender, EventArgs e);
public static event IsBusyHandler IsBusyChange;
}
Then you could do:
private void SaveCommand_Action()
{
StateManager.Process(() =>
{
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
}
}
StateManager.IsBusy = false;
});
}

Thanks to sa_ddam213, I have the issue under wraps. The problem was the priority. This code is what took care of it:
private void SaveCommand_Action()
{
StateManager.IsBusy = true;
Application.Current.Dispatcher.Invoke(() => Save(), System.Windows.Threading.DispatcherPriority.Background);
}
private void Save()
{
this.Validate();
if (!HasValidationErrors)
{
if (this.CustomerControlVM.SaveCustomer() != 0)
{
VehicleControlVM.VehicleModel.CustomerID = this.CustomerControlVM.CustomerModel.CustomerID;
this.VehicleControlVM.SaveVehicle();
ComplaintsView complaintsControl = new ComplaintsView();
(complaintsControl.DataContext as ComplaintsViewModel).CurrentVehicle = this.VehicleControlVM.VehicleModel;
(complaintsControl.DataContext as ComplaintsViewModel).CurrentCustomer = this.CustomerControlVM.CustomerModel;
StateManager.LoadView(complaintsControl, PageTransitionType.SlideLeft);
StateManager.IsBusy = false;
}
}
}
I have a little more work to do so I don't need to do this with each IsBusy state change, but with what I've learned, I can figure it out. Thanks so much sa_ddam213.

Related

Change data binding value in ViewModel xamarin forms android

I have a binding set up:
using System;
using System.Collections.Generic;
using System.Text;
using LoanApp2.Model;
using System.Windows.Input;
using System.Threading.Tasks;
using System.ComponentModel;
using LoanApp2.Views;
using Newtonsoft.Json;
using System.Net.Http;
namespace LoanApp2.ViewModel
{
public class LoginViewModel : INotifyPropertyChanged
{
// For data binding of activity indicator
string actIndVal = "False";
public string ActIndVal {
get => actIndVal;
set {
if(actIndVal == value)
{
return;
} else
{
actIndVal = value;
OnPropertyChanged(nameof(ActIndVal));
}
}
}
public static List<LoginBasicData> listLoginBasicData = new List<LoginBasicData>();
public LoginViewModel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(value));
}
public static async void VerifyClientID(string clientID)
{
// Start of HTTP Requests
using (HttpClient client = new HttpClient())
{
...
}
}
}
The default value would be false, in in the class VerifyClientID(), I want to call the ActIndVal and change it to true so that the Activity Indicator will be visible. And call it again in the bottom part of the VerifyClientID() class so that I can change the value to false again.
Change the property in the ViewModel won't work, you should change the value of ActIndVal of the ViewModel which is your BindContext.
For example:
public partial class MainPage : ContentPage
{
LoginViewModel myViewModel;
public MainPage()
{
InitializeComponent();
myViewModel = new LoginViewModel();
this.BindingContext = myViewModel;
}
private async void Button_Clicked(object sender, EventArgs e)
{
myViewModel.ActIndVal = true;
await myViewModel.VerifyClientID("clientID");
myViewModel.ActIndVal = false;
}
}
public class LoginViewModel : INotifyPropertyChanged
{
// For data binding of activity indicator
bool actIndVal = false;
public bool ActIndVal
{
get => actIndVal;
set
{
if (actIndVal == value)
{
return;
}
else
{
actIndVal = value;
OnPropertyChanged(nameof(ActIndVal));
}
}
}
public LoginViewModel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string value)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(value));
}
public async Task VerifyClientID(string clientID)
{
// Start of HTTP Requests
await Task.Delay(3000);
}
}
myViewModel.ActIndVal is the value you need to change because Frame.IsVisible is binding to this value. Changing the value of ActIndVal in your viewModel makes no sense.
I uploaded my sample project here and feel free to ask me any question.
first of all this should be a boolean to avoid casting issues
private bool actIndVal;
public bool ActIndVal {
get => actIndVal;
set {
if(actIndVal == value)
{
return;
} else
{
actIndVal = value;
OnPropertyChanged(nameof(ActIndVal));
}
}
}
Then in your method change the values:
public async Task VerifyClientID(string clientID)
{
ActIndVal= true;
//API CALL CODE
ActIndVal= false;
}
Then just bind this to your Indicator
<ActivityIndicator IsRunning="{Binding ActIndVal}" IsVisible="{Binding ActIndVal}" ....

ActivityIndicator on Xamarin Forms

How to put an ActivityIndicator on Xamarin Forms OnStart() function.
I am check Network access on OnStart() function.
Bind the ActivityIndicator to a property in your BaseViewModel (IsBusy).
View
<ActivityIndicator Color="Accent" IsVisible="{Binding IsBusy}" IsRunning="{Binding IsBusy}" />
BaseViewModel (Inherited by all ViewModels)
private bool _isBusy;
public bool IsBusy
{
get { return _isBusy; }
set
{
_isBusy = value;
OnPropertyChanged("IsBusy");
}
}
Get yourself a good MVVM framework (Prism) and put the network check in the OnNavigatedTo method for your start page.
public override void OnNavigatedTo(INavigationParameters parameters)
{
IsBusy = true;
await CheckNetwork();
IsBusy = false;
}
Now you can paste that same ActivityIndicator snippet into any page (XAML) that is bound to a ViewModel inheriting BaseViewModel and it will just work when you set IsBusy.
Haven't used ActivityIndicator, but this nuget works great: Acr.UserDialogs.
After installing and adding the initialization part in the MainActivity or ios equivalent, just add the following code between resource intensive threads in either your code-behind file or viewmodel (mvvm):
This works for code-behind file:
protected override async void OnAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
UserDialogs.Instance.ShowLoading();
//do stuff here
UserDialogs.Instance.HideLoading();
}
This works for FreshMVVM framework:
protected override async void ViewIsAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
UserDialogs.Instance.ShowLoading();
//do stuff here
UserDialogs.Instance.HideLoading();
}
I'm using network checking in my projects too, please check this:
using Plugin.Connectivity;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace PetBellies.View
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NoConnection : ContentPage
{
private bool wasNotConn = false;
public NoConnection()
{
InitializeComponent();
CrossConnectivity.Current.ConnectivityChanged += async (sender, args) =>
{
if (CrossConnectivity.Current.IsConnected && !wasNotConn)
{
wasNotConn = true;
await Navigation.PushModalAsync(new NavigationPage(new MainPage()));
}
else
{
wasNotConn = false;
}
};
}
public NoConnection(bool isFromLogin)
{
CrossConnectivity.Current.ConnectivityChanged += async (sender, args) =>
{
if (CrossConnectivity.Current.IsConnected && !wasNotConn)
{
wasNotConn = true;
var page = new LoginPage();
var navPage = new NavigationPage(page);
NavigationPage.SetHasNavigationBar(navPage, false);
await Navigation.PushModalAsync(navPage);
}
else
{
wasNotConn = false;
}
};
}
}
}
https://github.com/officialdoniald/PetBellies/blob/master/PetBellies/PetBellies/View/NoConnection.xaml.cs
If the connection lost, the application navigate to this page and stay on this page while the connection is unavailable.

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.

WPF percentage status shown in label MVVM

I got some problem in showing download percentage in GridView of WCF. I used MVVM pattern.
Here is my background worker in application start:
public partial class MainWindow : Window
{
public MainWindow()
{
Overall.EverythingOk = "Nothing";
InitializeComponent();
//IRepo repo = new Repo();
ViewModel.MainWindowsViewModel viewModel = new ViewModel.MainWindowsViewModel();
this.DataContext = viewModel;
BackGroundThread bgT = new BackGroundThread();
bgT.bgWrk.RunWorkerAsync();
}}
Here is the DoWork function in BackGroundTHread class
public void bw_DoWork(object sender, DoWorkEventArgs e)
{
if (!Overall.stopStatus)
{
for (int i=0; i < 10000; i++)
{
Overall.PercentageDwnd = i;
Overall.caseRefId = "999999";
if (i == 9998)
{
i = 1;
}
}
}
}
Overall.PercentageDwnd and Overall.caseRefId are static variable (you can call from everywhere in the application) and always update until the background worker completed. I got another ViewModel called TestViewModel and here it is.
public class TestViewModel:BindableBase
{
private String _UpdatePer=Overall.PercentageDwnd.ToString();
public String UpdatePercentage
{
get { return _UpdatePer; }
set { SetProperty(ref _UpdatePer, value); }
}
private ObservableCollection _ViewAKA = new ObservableCollection();
private tblTransaction model;
public TestViewModel(tblTransaction model)
{
// TODO: Complete member initialization
}
public ObservableCollection ViewAKA
{
get { return _ViewAKA; }
set { SetProperty(ref _ViewAKA, value); }
}
}
I bind with TestView.xaml file
<Window x:Class="EmployeeManager.View.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TestView" Height="359.774" Width="542.481">
<Grid Margin="0,0,2,0">
<Label Content="{Binding UpdatePercentage,UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Background="Red" Foreground="White" Margin="130,86,0,0" VerticalAlignment="Top" Width="132" Height="39">
</Label>
</Grid>
</Window>
There is no real time update at Label even though I bind UpdatePercentage to it. How can I update real time to label?
The problem is that you are updating the static properties, which are not bound to anything. You need to update and raise the property changed notification for the properties which are bound to the label controls, i.e. UpdatePercentage
Can you pass the TestViewModel instance into the RunWorkerAsync call?
bgT.bgWrk.RunWorkerAsync(testViewModel);
And then access in the DoWork event handler:
public void bw_DoWork(object sender, DoWorkEventArgs e)
{
if (!Overall.stopStatus)
{
var viewModel = e.Argument as TestViewModel;
for (int i=0; i < 10000; i++)
{
Overall.PercentageDwnd = i;
viewModel.UpdatePercentage = i;
Overall.caseRefId = "999999";
if (i == 9998)
{
i = 1;
}
}
}
}
Here is answer link:
https://social.msdn.microsoft.com/Forums/vstudio/en-US/02a7b9d1-1c26-4aee-a137-5455fee175b9/wpf-percentage-status-shown-in-label-mvvm?forum=wpf
i need to trigger when the Overall.PercentageDwnd property changes.
Edited
In Overall Class:
public class Overall
{
private static int _percentage;
public static int PercentageDwnd
{
get { return _percentage; }
set
{
_percentage = value;
//raise event:
if (PercentageDwndChanged != null)
PercentageDwndChanged(null, EventArgs.Empty);
}
}
public static string caseRefId { get; set; }
public static bool stopStatus { get; set; }
public static event EventHandler PercentageDwndChanged;
}
In TestViewModel:
public class TestViewModel : BindableBase
{
private String _UpdatePer = Overall.PercentageDwnd.ToString();
public String UpdatePercentage
{
get { return _UpdatePer; }
set { SetProperty(ref _UpdatePer, value); }
}
public TestViewModel(tblTransaction model)
{
Overall.PercentageDwndChanged += Overall_PercentageDwndChanged;
// TODO: Complete member initialization
}
private void Overall_PercentageDwndChanged(object sender, EventArgs e)
{
this.UpdatePercentage = Overall.PercentageDwnd.ToString();
}
}
Since you have bound the TextBlock in the view to the UpdatePercentage source property, you need to set this one and raise the PropertyChanged event whenever you want to update the Label in the view. This means that you need to know when the Overall.PercentageDwnd property changes.
Credit to
Magnus (MM8)
(MCC, Partner, MVP)
Thanks All

UITypeEditor is not closing properly

I am using a UserControl with a UITypeEditor. The user control has OK and Cancel buttons that do nothing except display a MessageBox with either OK or Cancel and then hide the user control. But when I click one of the buttons, the PropertyGrid displays an empty box where the UserControl was until I click away. Then the box disappears and the dialog is displayed.
Here is the user control code:
using System;
using System.Windows.Forms;
namespace j2associates.Tools.Winforms.Controls.DesignTimeSupport.SupportingClasses
{
public partial class SimpleTest : UserControl
{
public bool Cancelled { get; set; }
public SimpleTest()
{
InitializeComponent();
}
private void btnOK_Click(object sender, EventArgs e)
{
Cancelled = false;
this.Hide();
}
private void btnCancel_Click(object sender, EventArgs e)
{
Cancelled = true;
this.Hide();
}
}
}
Here is the UITypeEditor code:
using System;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using j2associates.Tools.Winforms.Controls.DesignTimeSupport.SupportingClasses;
namespace j2associates.Tools.Winforms.Controls.DesignTimeSupport.Editors
{
internal class TimeElementsEditor : UITypeEditor // PropertyEditorBase<TimeElementsUserControl>
{
public override UITypeEditorEditStyle GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if (value.GetType() == typeof(j2aTimePicker.TimeElementOptions))
{
var editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
if (editorService != null)
{
using (var st = new SimpleTest())
{
editorService.DropDownControl(st);
if (st.Cancelled)
{
MessageBox.Show("Cancel");
}
else
{
MessageBox.Show("OK");
}
editorService.CloseDropDown();
}
}
}
return value;
}
}
}
Any ideas and/or suggestions would be appreciated.
Sigh, it's always easy when you find it.
I needed to pass the IWindowsFormsEditorService in via an overloaded constructor, cache it and then call it's CloseDropdown method in the Button Click events instead of hiding the user control. It now works as expected.
/// <summary>
/// Displays an OK and Cancel button. When one is pressed,
/// the dialog is closed and a message box is displayed.
/// The actual value of the property is unchanged throughout.
/// </summary>
/// <remarks>The ToolboxItem attribute prevents the control from being displayed in the ToolKit.</remarks>
[ToolboxItem(false)]
public partial class SimpleTest : UserControl
{
public bool Cancelled { get; set; }
private IWindowsFormsEditorService m_EditorService;
// Require the use of the desired overloaded constructor.
private SimpleTest()
{
InitializeComponent();
}
internal SimpleTest(IWindowsFormsEditorService editorService)
: this()
{
// Cache the editor service.
m_EditorService = editorService;
}
private void btnOK_Click(object sender, EventArgs e)
{
Cancelled = false;
m_EditorService.CloseDropDown();
}
private void btnCancel_Click(object sender, EventArgs e)
{
Cancelled = true;
m_EditorService.CloseDropDown();
}
}
And here is the modified editor call:
using (var simpleTest = new SimpleTest(editorService))
{
editorService.DropDownControl(simpleTest);
MessageBox.Show(simpleTest.Cancelled ? "Cancelled" : "OK");
}

Categories