I'm building a Revit AddIn with WPF modeless dialogs and I want to use an ExternalEvent to retrieve Elements selected by the user. Is what I am doing viable and what do I need to change for it to work?
Since I don't have a valid API document context, I raise an ExternalEvent when a button is clicked to retrieve UniqueId of Elements that are currently selected.
Here are the relevant classes (I tried to reduce the code as much as I could) :
public class App : IExternalApplication {
internal static App _app = null;
public static App Instance => _app;
public Result OnStartup(UIControlledApplication application) {
_app = this;
return Result.Succeeded;
}
public void ShowWin(UIApplication ui_app) {
var eventHandler = new CustomEventHandler();
var externalEvent = ExternalEvent.Create(eventHandler);
var window = new WPFWindow(eventHandler, externalEvent);
Process proc = Process.GetCurrentProcess();
WindowInteropHelper helper = new WindowInteropHelper(window) {
Owner = proc.MainWindowHandle
};
window.Show();
}
}
public class AddIn : IExternalCommand {
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) {
App.Instance.ShowWin(commandData.Application);
return Result.Succeeded;
}
}
public class CustomEventHandler : IExternalEventHandler {
public event Action<List<string>> CustomEventHandlerDone;
public void Execute(UIApplication ui_app) {
UIDocument ui_doc = ui_app.ActiveUIDocument;
if (ui_doc == null) {
return;
}
Document doc = ui_doc.Document;
List<string> element_ids = null;
var ui_view = ui_doc.GetOpenUIViews().Where(x => x.ViewId == doc.ActiveView.Id).FirstOrDefault();
if (doc.ActiveView is View3D view3d && ui_view != null) {
using (Transaction tx = new Transaction(doc)) {
tx.Start();
element_ids = ui_doc.Selection.GetElementIds().Select(x => doc.GetElement(x)?.UniqueId).Where(x => x != null).ToList();
tx.Commit();
}
}
this.CustomEventHandlerDone?.Invoke(element_ids);
}
}
public partial class WPFWindow {
private CustomEventHandler _eventHandler;
private ExternalEvent _externalEvent;
public WPFWindow(CustomEventHandler eventHandler, ExternalEvent externalEvent) {
this._eventHandler = eventHandler;
this._eventHandler.CustomEventHandlerDone += this.WPFWindow_CustomEventDone;
this._externalEvent = externalEvent;
}
private void Button_Click(object sender, RoutedEventArgs e) {
this._externalEvent.Raise();
}
private void WPFWindow_CustomEventDone(List<string> element_ids) {
// this point is never reached while an element is selected
}
}
When an element is selected, the ExternalEvent is marked as pending but is only executed when the selection is cleared by the user.
The same happens with UIControlledApplication.Idling.
I would like for it to be executed even when elements are selected, or an alternative way to do it, and not involving PickObject.
I ran into the same problem.
I was able to determine that the problem occurs if elements of the same family are selected. Moreover, there is a certain threshold value, somewhere from 10 to 20 or more, at which this is manifested.
I was able to get around this by canceling the selection of elements UIDocument.Selection.SetElementIds(new List<ElementId>()) before calling ExternalEvent.Raise(). And then at the end return the selection, if necessary.
Related
I have this code in my MainWindow.
The user enters Name, Phone and Email in the fields provided, selects Location and then the Name appears in the listbox, lstClients.
I'm trying to write a method to remove a selected name from the listbox.
namespace WpfApp_Employment_Help
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
// client list
List<Client> ClientList = new List<Client>();
public MainWindow()
{
InitializeComponent();
}
// method to select location via radio button
public string GetSelectedLocation()
{
string selected = string.Empty;
if (RBLocE.IsChecked == true) { selected = "Edinburgh"; }
else if (RBLocG.IsChecked == true) { selected = "Glasgow"; }
else if (RBLocO.IsChecked == true) { selected = "Other"; }
return selected;
}
// method to create a new client on click
private void newClient(object sender, RoutedEventArgs e)
{
Client c = new Client(boxClientName.Text, boxClientPhone.Text, boxClientEmail.Text, GetSelectedLocation());
boxClientName.Clear();
boxClientPhone.Clear();
boxClientEmail.Clear();
ClientList.Add(c);
lstClients.ItemsSource = null;
lstClients.ItemsSource = ClientList;
}
// method to id selected client
private void AssignID(object sender, RoutedEventArgs e)
{
Client c = lstClients.SelectedItem as Client;
if (c != null)
{
c.AssignID();
}
lstClients.ItemsSource = null;
lstClients.ItemsSource = ClientList;
}
// method to remove selected client
private void RemoveClient(object sender, RoutedEventArgs e)
{
lstClients.Items.Remove(lstClients.SelectedItem);
}
}
}
When I run this code, I get Unhandled Exception:
System.InvalidOperationException: 'Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.'
how can I rewrite my RemoveClient method?
my code for the Client class is this:
public partial class Client
{
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public string Location { get; }
public bool IDed { get; private set; }
public Client(string n, string p, string e, string l)
{
Name = n;
Phone = p;
Email = e;
Location = l;
}
}
I have Visual Studio 2022 which has been recently updated.
I have also tried the following solution but it gives me another unhandled error?
It looks like I need to change List </string/> and string to something else. but what?
private void RemoveClient(object sender, EventArgs e)
{
if (lstClients.Items.Count >= 1)
{
if (lstClients.SelectedValue != null)
{
var items = (List<string>)lstClients.ItemsSource;
var item = (string)lstClients.SelectedValue;
lstClients.ItemsSource = null;
lstClients.Items.Clear();
items.Remove(item);
lstClients.ItemsSource = items;
}
}
else
{
System.Windows.Forms.MessageBox.Show("No ITEMS Found");
}
}
System.InvalidCastException: 'Unable to cast object of type 'System.Collections.Generic.List`1[WpfApp_Employment_Help.Client]' to type 'System.Collections.Generic.List`1[System.String]'.'
As the error message suggests, you can't modify the ItemsControl via the collection view returned from the ItemsControl.Items property.
WPF is generally designed to work on the data sources instead of handling the data related controls (data presentation). This way data and data presentation (GUI) are cleanly separated and code will become a lot simpler to write.
In case of the ListView (or ItemsControl in general), simply modify the source collection.
To improve performance, the source collection should be a INotifyCollectionChanged implementation, for example ObservableCollection<T>, especially when you expect to modify the source collection.
This makes invalidating the ItemsSource e.g. by assigning null, just to set it again redundant and significantly improves the performance.
public partial class MainWindow : Window
{
// client list
public ObservableCollection<Client> ClientList { get; } = new ObservableCollection<Client>();
// method to create a new client on click
private void newClient(object sender, RoutedEventArgs e)
{
Client c = new Client(boxClientName.Text, boxClientPhone.Text, boxClientEmail.Text, GetSelectedLocation());
boxClientName.Clear();
boxClientPhone.Clear();
boxClientEmail.Clear();
ClientList.Add(c);
// The following lines are no longer needed
// as the GUI is now notified about the collection changes (by the INotifyCollectionChanged collection)
//lstClients.ItemsSource = null;
//lstClients.ItemsSource = ClientList;
}
// method to id selected client
private void AssignID(object sender, RoutedEventArgs e)
{
Client c = lstClients.SelectedItem as Client;
// Same as the if-statement below
c?.AssignID();
//if (c != null)
//{
// c.AssignID();
//}
// The following lines are no longer needed
// as the GUI is now notified about the collection changes (by the INotifyCollectionChanged collection)
//lstClients.ItemsSource = null;
//lstClients.ItemsSource = ClientList;
}
// method to remove selected client
private void RemoveClient(object sender, RoutedEventArgs e)
{
var clientToRemove = lstClients.SelectedItem as Client;
this.ClientList.Remove(clientToRemove);
}
}
If you change the type of ClientList from List<Client> to ObservableCollection<Client>, you could simply remove the item directly from the source collection:
public partial class MainWindow : Window
{
ObservableCollection<Client> ClientList = new ObservableCollection<Client>();
public MainWindow()
{
InitializeComponent();
}
...
private void RemoveClient(object sender, RoutedEventArgs e)
{
ClientList.Remove(lstClients.SelectedItem as Client);
}
}
Is there a way to get a list of all windows in Avalonia?
The equivalent of this in WPF
Application.Current.Windows
My requirement is to activate or close a certain window based on its DataContext.
If I can't access such a list; is there a way to track the creation and destruction of windows to create an internal list?
you can create WindowsManagerClass with one static propery with type of List<Window> like this
public class WindowsManager
{
public static List<Window> AllWindows = new List<Window>();
}
and add to AllWindows like this code in your Form constructor
public MainWindow()
{
InitializeComponent();
WindowsManager.AllWindows.Add(this);
}
and where you need you can access reference like this
var allwindows = WindowsManager.AllWindows;
var selectedWindows = allwindows.FirstOrDefault(x => x.Name == "Test");
if (selectedWindows != null)
{
if (selectedWindows.IsActive)
{
selectedWindows.Close();
}
}
Full form code (in this example when you click button form will be close)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WindowsManager.AllWindows.Add(this);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var allwindows = WindowsManager.AllWindows;
var selectedWindows = allwindows.FirstOrDefault(x => x.Name == "");
if (selectedWindows != null)
{
if (selectedWindows.IsActive)
{
selectedWindows.Close();
}
}
}
}
You need IClassicDesktopStyleApplicationLifetime::Windows property. Lifetime is available from Application's ApplicationLifetime property.
e. g.
((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime).Windows
Note that it's not available for Mobile, WebAssembly and Linux framebuffer platforms.
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.
i got some little problems. I want to write some unittest fora c#/wpf/interactivty project with visual studio 2010. and dont forget im a beginner, so sorry for that ;)
the unittest should simulate a (virtual) Key Down Event on a textbox and the result should raise an action.(Action result: Console output - just to check as first step)
i still fixed 2 problems -> the dispatcher problem & the presentationSource bug.
The unittest still simulates the keyevent and the keyevent reached the textbox but the question is, why the action not raised through the keydown event on the textbox?
It's a threading problem? what's my missunderstand?
here is the code
The Unittest
at the end of the unittest u could check the textbox - the keyboard works
[TestMethod]
public void simpleTest()
{
var mockWindow = new MockWindow();
//simple test to check if the virtualKeyboard works
string CheckText = "Checktext";
mockWindow.SendToUIThread(mockWindow.textbox, CheckText);
mockWindow.SendToUIThread(mockWindow.textbox, "k");
//needed to start the dispatcher
DispatcherUtil.DoEvents();
}
the Dispatcher fix
public static class DispatcherUtil
{
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
private static object ExitFrame(object frame)
{
((DispatcherFrame)frame).Continue = false;
return null;
}
}
My Testaction
class TestAction : TriggerAction<UIElement>
{
protected override void Invoke(object parameter)
{
Console.WriteLine("testAction invoke");
}
}
The MockWindow
public class MockWindow : Window
{
public TextBox textbox { get; private set; }
public MockWindow()
{
//add a grid&textbox
Grid grid = new Grid();
textbox = new TextBox();
this.Content = grid;
grid.Children.Add(textbox);
//create the testaction/triggerEvent & add them
TestAction testAction = new TestAction();
System.Windows.Interactivity.EventTrigger TestTrigger = new System.Windows.Interactivity.EventTrigger();
TestTrigger.EventName = "KeyDown";
TestTrigger.Actions.Add(testAction);
TestTrigger.Attach(this.textbox);
}
//enter a keyboard press on an UIElement
public void SendToUIThread(UIElement element, string text)
{
element.Dispatcher.BeginInvoke(new Action(() =>
{
SendKeys.Send(element, text);
}), DispatcherPriority.Input);
}
}
the MockKeyboard added from codeplex sendkeys + a presentationCore fix for unittest(added at class SendKeys)
public class FixPresentationSource : PresentationSource
{
protected override CompositionTarget GetCompositionTargetCore()
{
return null;
}
public override Visual RootVisual { get; set; }
public override bool IsDisposed { get { return false; } }
}
In brief:
Is there any built-in function in .Net 2.0 to Expand TreeNodes when hovered over whilst a drag and drop operation is in progress?
I'm using C# in Visual Studio 2005.
In more detail:
I've populated a Treeview control with a multi levelled, multinoded tree (think organisational chart or file/folder dialog) and I want to use drag and drop to move nodes within the tree.
The drag drop code works well, and I can drop onto any visible node, however I would like my control to behave like Windows Explorer does when dragging files over the folder pane. Specifically, I'd like each folder to open if hovered over for 1/2 second or so.
I've begun developing a solution using Threading and a Sleep method but I'm running into problems and wondered if there was something in place already, if not I will knuckle down and learn how to use threading (it's about time, but I was hoping to get this app out quickly)
Do I need to write my own code to handle Expanding a TreeNode when hovered over in drag-drop mode?
You can use the DragOver event; it fires repeatedly while you are dragging an object
Opening after a delay can be done very easily with two extra variables that note the last object under the mouse and the time. No threading or other tricks required (lastDragDestination and lastDragDestinationTime in my example)
From my own code:
TreeNode lastDragDestination = null;
DateTime lastDragDestinationTime;
private void tvManager_DragOver(object sender, DragEventArgs e)
{
IconObject dragDropObject = null;
TreeNode dragDropNode = null;
//always disallow by default
e.Effect = DragDropEffects.None;
//make sure we have data to transfer
if (e.Data.GetDataPresent(typeof(TreeNode)))
{
dragDropNode = (TreeNode)e.Data.GetData(typeof(TreeNode));
dragDropObject = (IconObject)dragDropNode.Tag;
}
else if (e.Data.GetDataPresent(typeof(ListViewItem)))
{
ListViewItem temp (ListViewItem)e.Data.GetData(typeof(ListViewItem));
dragDropObject = (IconObject)temp.Tag;
}
if (dragDropObject != null)
{
TreeNode destinationNode = null;
//get current location
Point pt = new Point(e.X, e.Y);
pt = tvManager.PointToClient(pt);
destinationNode = tvManager.GetNodeAt(pt);
if (destinationNode == null)
{
return;
}
//if we are on a new object, reset our timer
//otherwise check to see if enough time has passed and expand the destination node
if (destinationNode != lastDragDestination)
{
lastDragDestination = destinationNode;
lastDragDestinationTime = DateTime.Now;
}
else
{
TimeSpan hoverTime = DateTime.Now.Subtract(lastDragDestinationTime);
if (hoverTime.TotalSeconds > 2)
{
destinationNode.Expand();
}
}
}
}
EDIT
I have a new solution, a bit far-fetched, but it works... It uses a DelayedAction class to handle delayed execution of an action on the main thread :
DelayedAction<T>
public class DelayedAction<T>
{
private SynchronizationContext _syncContext;
private Action<T> _action;
private int _delay;
private Thread _thread;
public DelayedAction(Action<T> action)
: this(action, 0)
{
}
public DelayedAction(Action<T> action, int delay)
{
_action = action;
_delay = delay;
_syncContext = SynchronizationContext.Current;
}
public void RunAfterDelay()
{
RunAfterDelay(_delay, default(T));
}
public void RunAfterDelay(T param)
{
RunAfterDelay(_delay, param);
}
public void RunAfterDelay(int delay)
{
RunAfterDelay(delay, default(T));
}
public void RunAfterDelay(int delay, T param)
{
Cancel();
InitThread(delay, param);
_thread.Start();
}
public void Cancel()
{
if (_thread != null && _thread.IsAlive)
{
_thread.Abort();
}
_thread = null;
}
private void InitThread(int delay, T param)
{
ThreadStart ts =
() =>
{
Thread.Sleep(delay);
_syncContext.Send(
(state) =>
{
_action((T)state);
},
param);
};
_thread = new Thread(ts);
}
}
AutoExpandTreeView
public class AutoExpandTreeView : TreeView
{
DelayedAction<TreeNode> _expandNode;
public AutoExpandTreeView()
{
_expandNode = new DelayedAction<TreeNode>((node) => node.Expand(), 500);
}
private TreeNode _prevNode;
protected override void OnDragOver(DragEventArgs e)
{
Point clientPos = PointToClient(new Point(e.X, e.Y));
TreeViewHitTestInfo hti = HitTest(clientPos);
if (hti.Node != null && hti.Node != _prevNode)
{
_prevNode = hti.Node;
_expandNode.RunAfterDelay(hti.Node);
}
base.OnDragOver(e);
}
}