WinForms Interop, Drag & Drop из WinForms → WPF
Я пытаюсь перетащить данные из части Winforms моего приложения в элементы управления WPF, которые содержатся внутри элемента ElementHost. И он падает, когда я пытаюсь это сделать.
Попытка того же самого, но от Winforms к Winforms работает нормально. (См. Пример кода ниже)
Мне нужна помощь в создании этой работы... есть ли какие-то подсказки, что я делаю неправильно?
Спасибо!
Пример:
В приведенном ниже примере кода я просто пытаюсь перетащить пользовательский объект MyContainerClass, созданный при запуске перетаскивания элемента управления ярлыком на 1) System.Windows.Forms.TextBox(Winforms) и 2) System.Windows.TextBox(WPF, добавленный к ElementHost).
Случай 1) работает отлично, но случай 2) сбой при попытке получить данные о переходе с помощью GetData(). GetDataPresent ( "WindowsFormsApplication1.MyContainerClass" ) возвращает "true", поэтому теоретически я должен иметь возможность извлекать мои данные о выбросах такого типа, как в Winforms.
Вот трассировка стека аварии:
"Error HRESULT E_FAIL has been returned from a call to a COM component" with the following stack trace:
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
at System.Windows.Forms.DataObject.GetDataIntoOleStructs(FORMATETC& formatetc, STGMEDIUM& medium)
at System.Windows.Forms.DataObject.System.Runtime.InteropServices.ComTypes.IDataObject.GetDataHere(FORMATETC& formatetc, STGMEDIUM& medium)
at System.Windows.Forms.DataObject.System.Runtime.InteropServices.ComTypes.IDataObject.GetData(FORMATETC& formatetc, STGMEDIUM& medium)
at System.Windows.DataObject.OleConverter.GetDataInner(FORMATETC& formatetc, STGMEDIUM& medium)
at System.Windows.DataObject.OleConverter.GetDataFromOleHGLOBAL(String format, DVASPECT aspect, Int32 index)
at System.Windows.DataObject.OleConverter.GetDataFromBoundOleDataObject(String format, DVASPECT aspect, Int32 index)
at System.Windows.DataObject.OleConverter.GetData(String format, Boolean autoConvert, DVASPECT aspect, Int32 index)
at System.Windows.DataObject.OleConverter.GetData(String format, Boolean autoConvert)
at System.Windows.DataObject.GetData(String format, Boolean autoConvert)
at System.Windows.DataObject.GetData(String format)
at WindowsFormsApplication1.Form1.textBox_PreviewDragEnter(Object sender, DragEventArgs e) in WindowsFormsApplication1\WindowsFormsApplication1\Form1.cs:line 48
Вот какой код:
// -- Add an ElementHost to your form --
// -- Add a label to your form --
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
System.Windows.Controls.TextBox textBox = new System.Windows.Controls.TextBox();
textBox.Text = "WPF TextBox";
textBox.AllowDrop = true;
elementHost2.Child = textBox;
textBox.PreviewDragEnter += new System.Windows.DragEventHandler(textBox_PreviewDragEnter);
System.Windows.Forms.TextBox wfTextBox = new System.Windows.Forms.TextBox();
wfTextBox.Text = "Winforms TextBox";
wfTextBox.AllowDrop = true;
wfTextBox.DragEnter += new DragEventHandler(wfTextBox_DragEnter);
Controls.Add(wfTextBox);
}
void wfTextBox_DragEnter(object sender, DragEventArgs e)
{
bool dataPresent = e.Data.GetDataPresent("WindowsFormsApplication1.MyContainerClass");
// NO CRASH here!
object data = e.Data.GetData("WindowsFormsApplication1.MyContainerClass");
}
void textBox_PreviewDragEnter(object sender, System.Windows.DragEventArgs e)
{
bool dataPresent = e.Data.GetDataPresent("WindowsFormsApplication1.MyContainerClass");
// Crash appens here!!
// {"Error HRESULT E_FAIL has been returned from a call to a COM component."}
object data = e.Data.GetData("WindowsFormsApplication1.MyContainerClass");
}
private void label1_MouseDown(object sender, MouseEventArgs e)
{
label1.DoDragDrop(new MyContainerClass(label1.Text), DragDropEffects.Copy);
}
}
public class MyContainerClass
{
public object Data { get; set; }
public MyContainerClass(object data)
{
Data = data;
}
}
Ответы
Ответ 1
@Pedery и jmayor: Спасибо за предложения, ребята! (см. мои выводы ниже)
После нескольких экспериментов, испытаний и ошибок и немного "Reflector'ing" мне удалось выяснить, почему именно я получил сообщение об ошибке: "Ошибка HRESULT E_FAIL была возвращена из вызова COM компонент".
Это было связано с тем, что при перетаскивании данных WPF ↔ Winforms в одном приложении данные должны быть Serializable!
Я проверил, насколько сложно было бы преобразовать все наши классы в "Serializable", и я бы по-настоящему пострадал по нескольким причинам... один, нам нужно было бы практически сделать все классы сериализуемые и два, некоторые из этих классов имеют ссылки на элементы управления! И элементы управления не могут быть сериализованы. Поэтому понадобился бы большой рефакторинг.
Итак... поскольку мы хотели передать любой объект любого класса для перетаскивания из/в WPF внутри одного и того же приложения, я решил создать класс-оболочку с атрибутом Serializable и реализовать ISerializable. У меня было бы 1 contructor с 1 параметром типа "объект", который был бы фактическим данными перетаскивания. Эта оболочка при сериализации/де-сериализации сериализует не сам объект... а скорее IntPtr для объекта (что мы можем сделать, так как мы хотим только эту функциональную функцию внутри нашего приложения только с одним экземпляром.) См. Пример кода ниже:
[Serializable]
public class DataContainer : ISerializable
{
public object Data { get; set; }
public DataContainer(object data)
{
Data = data;
}
// Deserialization constructor
protected DataContainer(SerializationInfo info, StreamingContext context)
{
IntPtr address = (IntPtr)info.GetValue("dataAddress", typeof(IntPtr));
GCHandle handle = GCHandle.FromIntPtr(address);
Data = handle.Target;
handle.Free();
}
#region ISerializable Members
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
GCHandle handle = GCHandle.Alloc(Data);
IntPtr address = GCHandle.ToIntPtr(handle);
info.AddValue("dataAddress", address);
}
#endregion
}
Чтобы сохранить функциональность IDataObject, я создал следующую оболочку DataObject:
public class DataObject : IDataObject
{
System.Collections.Hashtable _Data = new System.Collections.Hashtable();
public DataObject() { }
public DataObject(object data)
{
SetData(data);
}
public DataObject(string format, object data)
{
SetData(format, data);
}
#region IDataObject Members
public object GetData(Type format)
{
return _Data[format.FullName];
}
public bool GetDataPresent(Type format)
{
return _Data.ContainsKey(format.FullName);
}
public string[] GetFormats()
{
string[] strArray = new string[_Data.Keys.Count];
_Data.Keys.CopyTo(strArray, 0);
return strArray;
}
public string[] GetFormats(bool autoConvert)
{
return GetFormats();
}
private void SetData(object data, string format)
{
object obj = new DataContainer(data);
if (string.IsNullOrEmpty(format))
{
// Create a dummy DataObject object to retrieve all possible formats.
// Ex.: For a System.String type, GetFormats returns 3 formats:
// "System.String", "UnicodeText" and "Text"
System.Windows.Forms.DataObject dataObject = new System.Windows.Forms.DataObject(data);
foreach (string fmt in dataObject.GetFormats())
{
_Data[fmt] = obj;
}
}
else
{
_Data[format] = obj;
}
}
public void SetData(object data)
{
SetData(data, null);
}
#endregion
}
И мы используем вышеуказанные классы следующим образом:
myControl.DoDragDrop(new MyNamespace.DataObject(myNonSerializableObject));
// in the drop event for example
e.Data.GetData(typeof(myNonSerializableClass));
Я знаю, что знаю... это не очень красиво... но это делает то, что мы хотели. Мы также создали вспомогательный класс dragdrop, который маскирует создание DataObject и имеет шаблонные функции GetData для извлечения данных без каких-либо изменений... немного похоже:
myNonSerializableClass newObj = DragDropHelper.GetData<myNonSerializableClass>(e.Data);
Так что спасибо за ответы! Вы, ребята, дали мне хорошие идеи, где можно найти возможные решения!
-Oli
Ответ 2
У меня была "аналогичная" проблема некоторое время назад, поэтому я могу хотя бы сказать вам, что я узнал.
Кажется, что .Net прибегает к удалению OLE, когда операции перетаскивания выполняются, но в простейшем случае. По какой-то причине GetDataPresent в этих случаях будет успешным, и GetData потерпит неудачу. Это, тем не менее, озадачено тем фактом, что существует несколько версий IDataObject в инфраструктуре .Net.
В Windows Forms по умолчанию используется System.Windows.Forms.IDataObject. Однако в вашем случае вы можете попробовать вместо System.Runtime.InteropServices.ComTypes.IDataObject сделать снимок. Вы также можете проверить мое обсуждение здесь.
Надеюсь, что это поможет.
Ответ 3
Кажется замечательным с первого взгляда. Я попробовал, но получил некоторые ошибки в реализации.
Я начал исправлять некоторые ошибки, когда я решил искать что-то немного более упрощенное, у которого нет указателей (humm мне это не нравится, особенно с сборкой мусора, но я понятия не имею, может ли это иметь реальное воздействие) и которые не используют Interop.
Я придумал это. Это работает для меня, и я надеюсь, что это сработает для кого-то еще. Он предназначен только для локального перетаскивания (внутри одного и того же приложения).
Как использовать для перетаскивания:
DragDrop.DoDragDrop(listBoxOfAvailableScopes, new DragDropLocal(GetSelectedSimulResultScopes()),
DragDropEffects.Copy);
Как использовать для удаления (получения):
DragDropLocal dragDropLocal = (DragDropLocal)e.Data.GetData(typeof(DragDropLocal));
SimulResultScopes simulResultScopes = (SimulResultScopes)dragDropLocal.GetObject();
код:
namespace Util
{
[Serializable]
public class DragDropLocal
{
private static readonly Dictionary<Guid, object> _dictOfDragDropLocalKeyToDragDropSource = new Dictionary<Guid, object>();
private Guid _guid = Guid.NewGuid();
public DragDropLocal(object objToDrag)
{
_dictOfDragDropLocalKeyToDragDropSource.Add(_guid, objToDrag);
}
public object GetObject()
{
object obj;
_dictOfDragDropLocalKeyToDragDropSource.TryGetValue(_guid, out obj);
return obj;
}
~DragDropLocal()
{
_dictOfDragDropLocalKeyToDragDropSource.Remove(_guid);
}
}
}
Ответ 4
Возможно, события противоположны. PreviewDragEnter должен быть связан с WPFTextBox. Также обратите внимание на класс DragEventArgs. Есть один в System.Windows.Form(версия Windows Form) и один в System.Windows(для версии WPF).