Say I have a BindingList<Person> where Person has a public string property called Name. Is there a way to effectively (if not directly) bind to the Current property (which is a Person object) and then index into the Name property?
I'm imagining a binding set up like
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource, "Current.Name", true));
or
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource.Current, "Name", true));
Both of these approaches produce runtime errors.
Currently, I am simply subscribing to the BindingList's CurrentChanged event and handling updates in there. This works but I would prefer the DataBinding approach if possible.
Using the NestedBindingProxy class provided below, you can do
nameLabel.DataBindings.Add(
new Binding("Text", new NestedBindingProxy(this.myBindingListSource, "Current.Name"), true));
Below is the c# code for NestedBindingProxy. The problem with WinForms data binding is it doesn't detect value changes when you use a navigation path that contains multiple properties. WPF does this though. So I created the class NestedBindingProxy that does the change detection and it exposes a property called "Value" that the windows binding can bind too. Whenever any property changes in the navigation path, the notify property changed event will fire for the "Value" property.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
namespace WindowsFormsApplication4
{
public sealed class NestedBindingProxy : INotifyPropertyChanged
{
class PropertyChangeListener
{
private readonly PropertyDescriptor _prop;
private readonly WeakReference _prevOb = new WeakReference(null);
public event EventHandler ValueChanged;
public PropertyChangeListener(PropertyDescriptor property)
{
_prop = property;
}
public object GetValue(object obj)
{
return _prop.GetValue(obj);
}
public void SubscribeToValueChange(object obj)
{
if (_prop.SupportsChangeEvents)
{
_prop.AddValueChanged(obj, ValueChanged);
_prevOb.Target = obj;
}
}
public void UnsubsctribeToValueChange()
{
var prevObj = _prevOb.Target;
if (prevObj != null)
{
_prop.RemoveValueChanged(prevObj, ValueChanged);
_prevOb.Target = null;
}
}
}
private readonly object _source;
private PropertyChangedEventHandler _subscribers;
private readonly List<PropertyChangeListener> _properties = new List<PropertyChangeListener>();
private readonly SynchronizationContext _synchContext;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
bool hadSubscribers = _subscribers != null;
_subscribers += value;
bool hasSubscribers = _subscribers != null;
if (!hadSubscribers && hasSubscribers)
{
ListenToPropertyChanges(true);
}
}
remove
{
bool hadSubscribers = _subscribers != null;
_subscribers -= value;
bool hasSubscribers = _subscribers != null;
if (hadSubscribers && !hasSubscribers)
{
ListenToPropertyChanges(false);
}
}
}
public NestedBindingProxy(object source, string nestedPropertyPath)
{
_synchContext = SynchronizationContext.Current;
_source = source;
var propNames = nestedPropertyPath.Split('.');
Type type = source.GetType();
foreach (var propName in propNames)
{
var prop = TypeDescriptor.GetProperties(type)[propName];
var propChangeListener = new PropertyChangeListener(prop);
_properties.Add(propChangeListener);
propChangeListener.ValueChanged += (sender, e) => OnNestedPropertyChanged(propChangeListener);
type = prop.PropertyType;
}
}
public object Value
{
get
{
object value = _source;
foreach (var prop in _properties)
{
value = prop.GetValue(value);
if (value == null)
{
return null;
}
}
return value;
}
}
private void ListenToPropertyChanges(bool subscribe)
{
if (subscribe)
{
object value = _source;
foreach (var prop in _properties)
{
prop.SubscribeToValueChange(value);
value = prop.GetValue(value);
if (value == null)
{
return;
}
}
}
else
{
foreach (var prop in _properties)
{
prop.UnsubsctribeToValueChange();
}
}
}
private void OnNestedPropertyChanged(PropertyChangeListener changedProperty)
{
ListenToPropertyChanges(false);
ListenToPropertyChanges(true);
var subscribers = _subscribers;
if (subscribers != null)
{
if (_synchContext != SynchronizationContext.Current)
{
_synchContext.Post(delegate { subscribers(this, new PropertyChangedEventArgs("Value")); }, null);
}
else
{
subscribers(this, new PropertyChangedEventArgs("Value"));
}
}
}
}
}
Try this:
Binding bind = new Binding("Text", myBindingListSource, "Current");
bind.Format += (s,e) => {
e.Value = e.Value == null ? "" : ((Person)e.Value).Name;
};
nameLabel.DataBindings.Add(bind);
I haven't tested it but it should work and I've been waiting for the feedback from you.
I stumbled across this by accident (four years after the original post), and after a quick read, I find that the accepted answer appears to be really over-engineered in respect to the OP's question.
I use this approach, and have posted an answer here to (hopefully) help anyone else who has the same problem.
The following should work (if infact this.myBindingListSource implements IBindingList)
I would just create a binding source to wrap my binding list (in my Form/View)
BindingSource bindingSource = new BindingSource(this.myBindingListSource, string.Empty);
And then just bind to the binding source like this:
nameLabel.DataBindings.Add(new Binding("Text", bindingSource, "Name"));
I think the OP's original code didn't work because there is no member called 'Current' on a BindingList (unless the OP has some sort of specialised type not mentioned).
Related
I am trying to create a Datagrid matrix that adds columns with Bindings during runtime.
I'm working in Visual Studio 2019, with C#, WPF and MVVM.
The matrix looks like this:
The fist three rows are created inside the .xaml of the View, while the last two columns are created dynamically. In another View there's a similar datagrid and each row added there results in a column in this matrix.
The usual columns are created like this:
<DataGridTextColumn Header="Name" Binding="{Binding EntryName, UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
The dynamically added columns are added like this inside of the code behind file of the View:
foreach(IControlRoleViewModel item in vm.ControlRoleTable)
{
if (item.EntryName != null)
{
MyGrid.Columns.Add(new DataGridCheckBoxColumn { Header = item.EntryName });
}
}
Since the new columns are getting dynamically added I'm not sure how to create dynamic bindings. When I'm using an array I have to define how long it is, and when I tried using a list or an ObservableCollection I failed because of other circumstances.
I need the information of the checkbox in the corresponding row. I already get the information which row is clicked, through the ObservableCollection representing the whole DataGrid.
What I tried was this:
foreach(IControlRoleViewModel item in vm.ControlRoleTable)
{
if (item.EntryName != null)
{
_newEntry = item.EntryName + "_IsTrue";
//send Binding Definition to ViewModel
vm.newEntry.Add(_newEntry); //vm.newEntry is a List<string> in the ViewModel
MyGrid.Columns.Add(new DataGridCheckBoxColumn { Header = item.EntryName, Binding = new Binding(_newEntry) });
}
}
That way I got the information of the new Binding into the ViewModel. But since the DataGrid representing the whole Matrix is defined like this:
<DataGrid Name="MyGrid" ItemsSource="{Binding ProceduresTable }" SelectionMode="Single" SelectedItem="{Binding SelectedEntryProcedures}" AutoGenerateColumns="false" CanUserAddRows="True" IsReadOnly="False" >
The Binding could not be found inside of the ObservableCollection<IProceduresTable> ProceduresTable.
After that I tried creating a new ObservableCollection inside of the IProceduresTable Interface. But that failed since I couldn't find out how to get a binding into an ObservableCollection that itself is inside an ObservableCollection.
Is there an easier way to do this?
For such cases I use my GenericRow and GenericTable classes:
You can use these two classes to create dynamic rows and columns. Using GenericRow class, you can generate row with desired property name and you can use the same property name for the columns for suitable binding.
public class GenericRow : CustomTypeDescriptor, INotifyPropertyChanged
{
#region Private Fields
List<PropertyDescriptor> _property_list = new List<PropertyDescriptor>();
#endregion
#region INotifyPropertyChange Implementation
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChange Implementation
#region Public Methods
public void SetPropertyValue<T>(string propertyName, T propertyValue)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception("The property doesn't exist.");
}
var property = properties.First();
property.SetValue(this, propertyValue);
OnPropertyChanged(propertyName);
}
public T GetPropertyValue<T>(string propertyName)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception("The property doesn't exist.");
}
var property = properties.First();
return (T)property.GetValue(this);
}
public void AddProperty<T, U>(string propertyName) where U : GenericRow
{
var customProperty =
new CustomPropertyDescriptor<T>(
propertyName,
typeof(U));
_property_list.Add(customProperty);
}
#endregion
#region Overriden Methods
public override PropertyDescriptorCollection GetProperties()
{
var properties = base.GetProperties();
return new PropertyDescriptorCollection(
properties.Cast<PropertyDescriptor>()
.Concat(_property_list).ToArray());
}
#endregion
}
and Generic Table:
public class GenericTable
{
private string tableName = "";
public string TableName
{
get { return tableName; }
set { tableName = value; }
}
private ObservableCollection<DataGridColumn> columnCollection;
public ObservableCollection<DataGridColumn> ColumnCollection
{
get { return columnCollection; }
private set { columnCollection = value; }
}
private ObservableCollection<GenericRow> genericRowCollection;
public ObservableCollection<GenericRow> GenericRowCollection
{
get { return genericRowCollection; }
set { genericRowCollection = value; }
}
public GenericTable(string tableName)
{
this.TableName = tableName;
ColumnCollection = new ObservableCollection<DataGridColumn>();
GenericRowCollection = new ObservableCollection<GenericRow>();
}
/// <summary>
/// ColumnName is also binding property name
/// </summary>
/// <param name="columnName"></param>
public void AddColumn(string columnName)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Header = columnName;
column.Binding = new Binding(columnName);
ColumnCollection.Add(column);
}
public override string ToString()
{
return TableName;
}
}
And for the XAML side:
<DataGrid Name="dataGrid"
local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
AutoGenerateColumns="False"
...>
and finally DataGridColumsBehaior :
public class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = source as DataGrid;
ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
dataGrid.Columns.Clear();
if (columns == null)
{
return;
}
foreach (DataGridColumn column in columns)
{
dataGrid.Columns.Add(column);
}
columns.CollectionChanged += (sender, e2) =>
{
NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
if (ne.Action == NotifyCollectionChangedAction.Reset)
{
dataGrid.Columns.Clear();
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Move)
{
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (DataGridColumn column in ne.OldItems)
{
dataGrid.Columns.Remove(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Replace)
{
dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
}
};
}
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
}
}
In the end, you can create GenericRows and add them into GenericTable. It is like a small viewmodel. In XAML side, dont forget to bind rowscollection as itemsource of the DataGrid.
I am trying to create a SwitchCell with a list of Elements.
Eventhough I found out how to do that with a plain string-List thanks to stackoverflow I can't find out what I'm doing wrong when I try to bind the Cell-Properties to a self-made struct.
This is my code:
public class RestaurantFilter
{
public List<FilterElement> Types;
public RestaurantFilter(List<string> types)
{
Types = new List<FilterElement>();
foreach (string type in types)
Types.Add(new FilterElement { Name = type, Enabled = false });
}
}
public struct FilterElement
{
public string Name;
public bool Enabled;
}
public FilterPage()
{
List<string> l = new List<string>(new string[] { "greek", "italian", "bavarian" });
RestaurantFilter filter = new RestaurantFilter(l);
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(() =>
{
var cell = new SwitchCell();
cell.SetBinding(SwitchCell.TextProperty, "Name");
cell.SetBinding(SwitchCell.IsEnabledProperty, "Enabled");
return cell;
});
types.ItemsSource = filter.Types;
Content = types;
}
But the SwitchCell's in the Application do not show the Name or the Boolean.
About the IsEnabledProperty - there seem to be a knonw bug with the IsEnabled property that will be fixed in the Xamarin.Forms 2.3.0-pre1 release so that might be related to your case:
https://bugzilla.xamarin.com/show_bug.cgi?id=25662
About the Name property - try changing your FilterElement struct to a class with properties and PropertyChangedEventHandler like this and it will work:
public class FilterElement
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
}
private bool _enabled;
public bool Enabled
{
get { return _enabled; }
set
{
_enabled = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Enabled"));
}
}
}
}
That way you will be able to update the Types list and it will automatically update the ListView.
By the way, if you want to turn the filter on or off based on your ViewModels (not enable or disable it), you need to use the OnProperty for the binding:
https://developer.xamarin.com/api/field/Xamarin.Forms.SwitchCell.OnProperty/
I'm writing a plugin for resharper which I want to use to navigate from a ConcreteCommand -> ConcreteCommandHandler where those types look like this
public class ConcreteCommand : ICommand
public class ConcreteCommandHandler : ICommandHandler<ConcreteCommand>
I've got as far as adding my navigation menu option when the cursor is on a ICommand instance/definition (currently only by checking if the name contains 'Command' and not 'CommandHandler'), and I think I have the code necessary to actually search for a type which inherits something, but my issue is that the only thing I actually have a type for is my ConcereteCommand and I need to create (or get a reference to) the generic type ICommandHandler<T> with T being the type the cursor is currently on.
So I have 2 things I still want to know:
How can I check if my IDeclaredElement is an implementation of a particular interface (ideally by specifying the full name in a string from config)?
How can I create a ITypeElement which is a generic type of a specific interface where I can set the generic type from my existing IDeclaredElements type, so I can then find classes which inherit this?
My existing code looks like this:
[ContextNavigationProvider]
public class CommandHandlerNavigationProvider : INavigateFromHereProvider
{
public IEnumerable<ContextNavigation> CreateWorkflow(IDataContext dataContext)
{
ICollection<IDeclaredElement> declaredElements = dataContext.GetData(DataConstants.DECLARED_ELEMENTS);
if (declaredElements != null || declaredElements.Any())
{
IDeclaredElement declaredElement = declaredElements.First();
if (IsCommand(declaredElement))
{
var solution = dataContext.GetData(JetBrains.ProjectModel.DataContext.DataConstants.SOLUTION);
yield return new ContextNavigation("This Command's &handler", null, NavigationActionGroup.Other, () => { GotToInheritor(solution,declaredElement); });
}
}
}
private void GotToInheritor(ISolution solution, IDeclaredElement declaredElement)
{
var inheritorsConsumer = new InheritorsConsumer();
SearchDomainFactory searchDomainFactory = solution.GetComponent<SearchDomainFactory>();
//How can I create the ITypeElement MyNameSpace.ICommandHandler<(ITypeElement)declaredElement> here?
solution.GetPsiServices().Finder.FindInheritors((ITypeElement)declaredElement, searchDomainFactory.CreateSearchDomain(solution, true), inheritorsConsumer, NullProgressIndicator.Instance);
}
private bool IsCommand(IDeclaredElement declaredElement)
{
//How can I check if my declaredElement is an implementation of ICommand here?
string className = declaredElement.ShortName;
return className.Contains("Command")
&& !className.Contains("CommandHandler");
}
}
Ok managed to work this out with a fair bit of pushing in the right direction from #CitizenMatt.
basically my solution looks like this (still needs some tidying up)
private static readonly List<HandlerMapping> HandlerMappings = new List<HandlerMapping>
{
new HandlerMapping("HandlerNavigationTest.ICommand", "HandlerNavigationTest.ICommandHandler`1", "HandlerNavigationTest"),
new HandlerMapping("HandlerNavTest2.IEvent", "HandlerNavTest2.IEventHandler`1", "HandlerNavTest2")
};
public IEnumerable<ContextNavigation> CreateWorkflow(IDataContext dataContext)
{
ICollection<IDeclaredElement> declaredElements = dataContext.GetData(DataConstants.DECLARED_ELEMENTS);
if (declaredElements != null && declaredElements.Any())
{
IDeclaredElement declaredElement = declaredElements.First();
ISolution solution = dataContext.GetData(JetBrains.ProjectModel.DataContext.DataConstants.SOLUTION);
ITypeElement handlerType = GetHandlerType(declaredElement);
if (handlerType != null)
{
yield return new ContextNavigation("&Handler", null, NavigationActionGroup.Other, () => GoToInheritor(solution, declaredElement as IClass, dataContext, handlerType));
}
}
}
private static ITypeElement GetHandlerType(IDeclaredElement declaredElement)
{
var theClass = declaredElement as IClass;
if (theClass != null)
{
foreach (IPsiModule psiModule in declaredElement.GetPsiServices().Modules.GetModules())
{
foreach (var handlerMapping in HandlerMappings)
{
IDeclaredType commandInterfaceType = TypeFactory.CreateTypeByCLRName(handlerMapping.HandledType, psiModule, theClass.ResolveContext);
ITypeElement typeElement = commandInterfaceType.GetTypeElement();
if (typeElement != null)
{
if (theClass.IsDescendantOf(typeElement))
{
IDeclaredType genericType = TypeFactory.CreateTypeByCLRName(handlerMapping.HandlerType, psiModule, theClass.ResolveContext);
ITypeElement genericTypeElement = genericType.GetTypeElement();
return genericTypeElement;
}
}
}
}
}
return null;
}
private static void GoToInheritor(ISolution solution, IClass theClass, IDataContext dataContext, ITypeElement genericHandlerType)
{
var inheritorsConsumer = new InheritorsConsumer();
var searchDomainFactory = solution.GetComponent<SearchDomainFactory>();
IDeclaredType theType = TypeFactory.CreateType(theClass);
IDeclaredType commandHandlerType = TypeFactory.CreateType(genericHandlerType, theType);
ITypeElement handlerTypeelement = commandHandlerType.GetTypeElement();
solution.GetPsiServices().Finder.FindInheritors(handlerTypeelement, searchDomainFactory.CreateSearchDomain(solution, true),
inheritorsConsumer, NullProgressIndicator.Instance);
var potentialNavigationPoints = new List<INavigationPoint>();
foreach (ITypeElement inheritedInstance in inheritorsConsumer.FoundElements)
{
IDeclaredType[] baseClasses = inheritedInstance.GetAllSuperTypes();
foreach (IDeclaredType declaredType in baseClasses)
{
if (declaredType.IsInterfaceType())
{
if (declaredType.Equals(commandHandlerType))
{
var navigationPoint = new DeclaredElementNavigationPoint(inheritedInstance);
potentialNavigationPoints.Add(navigationPoint);
}
}
}
}
if (potentialNavigationPoints.Any())
{
NavigationOptions options = NavigationOptions.FromDataContext(dataContext, "Which handler do you want to navigate to?");
NavigationManager.GetInstance(solution).Navigate(potentialNavigationPoints, options);
}
}
public class InheritorsConsumer : IFindResultConsumer<ITypeElement>
{
private const int MaxInheritors = 50;
private readonly HashSet<ITypeElement> elements = new HashSet<ITypeElement>();
public IEnumerable<ITypeElement> FoundElements
{
get { return elements; }
}
public ITypeElement Build(FindResult result)
{
var inheritedElement = result as FindResultInheritedElement;
if (inheritedElement != null)
return (ITypeElement) inheritedElement.DeclaredElement;
return null;
}
public FindExecution Merge(ITypeElement data)
{
elements.Add(data);
return elements.Count < MaxInheritors ? FindExecution.Continue : FindExecution.Stop;
}
}
And this allows me no navigate to multiple handlers if they exist. This currently relies on the interfaces for the handled type and the handler type being in the same assembly. But this seems reasonable enough for me at the moment.
I would like to store usersettings. They are created at runtime and should be read after restarting the application.
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
var property = new SettingsProperty("Testname");
property.DefaultValue = "TestValue";
Settings.Default.Properties.Add(property);
Settings.Default.Save();
}
At this point, the setting is stored and I can access it.
After restarting the application, the newly created setting is away:
public MainForm()
{
InitializeComponent();
foreach (SettingsProperty property in Settings.Default.Properties)
{
//Setting which was created on runtime before not existing
}
}
Trying this piece: Settings.Default.Reload(); didn't affect anything on the outcome. I tried also other things like described here, but neither of them worked for me.
Probably a bit late for you but for others there are 2 parts.
Saving the new UserSetting
Reloading from userConfig.xml at startup
I created this extension for ApplicationSettingsBase based on other answers
public static void Add<T>(this ApplicationSettingsBase settings, string propertyName, T val)
{
var p = new SettingsProperty(propertyName)
{
PropertyType = typeof(T),
Provider = settings.Providers["LocalFileSettingsProvider"],
SerializeAs = SettingsSerializeAs.Xml
};
p.Attributes.Add(typeof(UserScopedSettingAttribute), new UserScopedSettingAttribute());
settings.Properties.Add(p);
settings.Reload();
//finally set value with new value if none was loaded from userConfig.xml
var item = settings[propertyName];
if (item == null)
{
settings[propertyName] = val;
settings.Save();
}
}
This will make Settings["MyKey"] work, but when you restart the setting will not be loaded, but the userConfig.xml has the new value (if you called Settings.Save())
The trick to get it to reload is to execute Add again e.g
if (settings.Properties.Cast<SettingsProperty>().All(s => s.Name != propertyName))
{
settings.Add("MyKey", 0);
};
The way Add works is that it will only set MyKey to 0 if no value is loaded from userConfig.xml
Just a tiny bit later:
I was having the same problem now and the answer of Tom was the only functioning hint. But since it was missing a bit of detail I want to share with you my solution for this.
using System.Configuration;
public static class ApplicationSettingsBaseExtension
{
public static void Add<T>(this ApplicationSettingsBase settings, string propertyName, T val)
{
bool itemExists = false;
foreach (SettingsProperty property in settings.Properties)
{
if (property.Name == propertyName)
{
itemExists = true;
break;
}
}
if (!itemExists)
{
var p = new SettingsProperty(propertyName)
{
PropertyType = typeof(T),
Provider = settings.Providers["LocalFileSettingsProvider"],
SerializeAs = SettingsSerializeAs.Xml
};
p.Attributes.Add(typeof(UserScopedSettingAttribute), new UserScopedSettingAttribute());
settings.Properties.Add(p);
settings.Reload();
}
settings[propertyName] = val;
settings.Save();
}
public static T Get<T>(this ApplicationSettingsBase settings, string propertyName, T defaultValue)
{
bool itemExists = false;
foreach (SettingsProperty property in settings.Properties)
{
if (property.Name == propertyName)
{
itemExists = true;
break;
}
}
if (!itemExists)
{
var p = new SettingsProperty(propertyName)
{
PropertyType = typeof(T),
Provider = settings.Providers["LocalFileSettingsProvider"],
SerializeAs = SettingsSerializeAs.Xml
};
p.Attributes.Add(typeof(UserScopedSettingAttribute), new UserScopedSettingAttribute());
settings.Properties.Add(p);
settings.Reload();
}
//finally set value with new value if none was loaded from userConfig.xml
var item = settings[propertyName];
if (item == null)
{
settings[propertyName] = defaultValue;
settings.Save();
}
return (T)settings[propertyName];
}
}
And with that you can use:
Properties.Settings.Default.Add(settingName, settingValue);
Properties.Settings.Default.Save();
...
setting = Settings.Default.Get(settingName, "");
In my case "setting" was a string, but it should work with all base types.
Note that these settings are not included in Settings.Default.Upgrade().
I hope I could help someone.
I use FlowDocument with BlockUIContainer and InlineUIContainer elements containing (or as base classes) some custom blocks - SVG, math formulas etc.
Because of that using Selection.Load(stream, DataFormats.XamlPackage) wont work as the serialization will drop the contents of *UIContainers except if the Child property is an image as available in Microsoft reference source:
private static void WriteStartXamlElement(...)
{
...
if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) &&
(blockUIContainer == null || !(blockUIContainer.Child is Image)))
{
...
elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true);
}
...
}
The only option in this case is to use is to use XamlWriter.Save and XamlReader.Load which are working flawlessly, serialize and deserialize all required properties and objects of a FlowDocument yet the Copy+Paste must be implemented manually as default implementation of Copy+Paste uses Selection.Load/Save.
Copy/Paste is critical as it is also used to handle dragging of elements in or between RichTextBox controls - the only way objects can be manipulated without custom dragging code.
This is why I am looking to implement copy/paste using a FlowDocument serialization but unfortunately there are some issues with it:
In current solution a whole FlowDocument object needs to be serialized/deserialized. Performance-wise it should not be a problem but I need to store information what selection range needs to be pasted from it (CustomRichTextBoxTag class).
Apparently objects cannot be removed from one document and added to another (a dead-end I discovered recently): 'InlineCollection' element cannot be inserted in a tree because it is already a child of a tree.
[TextElementCollection.cs]
public void InsertAfter(TextElementType previousSibling, TextElementType newItem)
{
...
if (previousSibling.Parent != this.Parent)
throw new InvalidOperationException(System.Windows.SR.Get("TextElementCollection_PreviousSiblingDoesNotBelongToThisCollection", new object[1]
{
(object) previousSibling.GetType().Name
}));
...
}
I think about setting FrameworkContentElement._parent using reflection in all elements which need to be moved to another document but that's a last resort hackish and dirty solution:
In theory I can copy only required objects: (optional) partial run with text at the beginning of selection, all paragraphs and inlines in between and and (possibly) partial run at the end, encapsulate these in a custom class and serialize/deserialize using XamlReader/XamlWriter.
Another solution I didn't think about.
Here is the custom RichTextBox control implementation with partially working custom Copy/Paste code:
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
namespace FlowMathTest
{
public class CustomRichTextBoxTag: DependencyObject
{
public static readonly DependencyProperty SelectionStartProperty = DependencyProperty.Register(
"SelectionStart",
typeof(int),
typeof(CustomRichTextBoxTag));
public int SelectionStart
{
get { return (int)GetValue(SelectionStartProperty); }
set { SetValue(SelectionStartProperty, value); }
}
public static readonly DependencyProperty SelectionEndProperty = DependencyProperty.Register(
"SelectionEnd",
typeof(int),
typeof(CustomRichTextBoxTag));
public int SelectionEnd
{
get { return (int)GetValue(SelectionEndProperty); }
set { SetValue(SelectionEndProperty, value); }
}
}
public class CustomRichTextBox: RichTextBox
{
public CustomRichTextBox()
{
DataObject.AddCopyingHandler(this, OnCopy);
DataObject.AddPastingHandler(this, OnPaste);
}
protected override void OnSelectionChanged(RoutedEventArgs e)
{
base.OnSelectionChanged(e);
var tag = Document.Tag as CustomRichTextBoxTag;
if(tag == null)
{
tag = new CustomRichTextBoxTag();
Document.Tag = tag;
}
tag.SelectionStart = Document.ContentStart.GetOffsetToPosition(Selection.Start);
tag.SelectionEnd = Document.ContentStart.GetOffsetToPosition(Selection.End);
}
private void OnCopy(object sender, DataObjectCopyingEventArgs e)
{
if(e.DataObject != null)
{
e.Handled = true;
var ms = new MemoryStream();
XamlWriter.Save(Document, ms);
e.DataObject.SetData(DataFormats.Xaml, ms);
}
}
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
var xamlData = e.DataObject.GetData(DataFormats.Xaml) as MemoryStream;
if(xamlData != null)
{
xamlData.Position = 0;
var fd = XamlReader.Load(xamlData) as FlowDocument;
if(fd != null)
{
var tag = fd.Tag as CustomRichTextBoxTag;
if(tag != null)
{
InsertAt(Document, Selection.Start, Selection.End, fd, fd.ContentStart.GetPositionAtOffset(tag.SelectionStart), fd.ContentStart.GetPositionAtOffset(tag.SelectionEnd));
e.Handled = true;
}
}
}
}
public static void InsertAt(FlowDocument destDocument, TextPointer destStart, TextPointer destEnd, FlowDocument sourceDocument, TextPointer sourceStart, TextPointer sourceEnd)
{
var destRange = new TextRange(destStart, destEnd);
destRange.Text = string.Empty;
// insert partial text of the first run in the selection
if(sourceStart.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
var sourceRange = new TextRange(sourceStart, sourceStart.GetNextContextPosition(LogicalDirection.Forward));
destStart.InsertTextInRun(sourceRange.Text);
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
destStart = destStart.GetNextContextPosition(LogicalDirection.Forward);
}
var field = typeof(FrameworkContentElement).GetField("_parent", BindingFlags.NonPublic | BindingFlags.Instance);
while(sourceStart != null && sourceStart.CompareTo(sourceEnd) <= 0 && sourceStart.Paragraph != null)
{
var sourceInline = sourceStart.Parent as Inline;
if(sourceInline != null)
{
sourceStart.Paragraph.Inlines.Remove(sourceInline);
if(destStart.Parent is Inline)
{
field.SetValue(sourceInline, null);
destStart.Paragraph.Inlines.InsertAfter(destStart.Parent as Inline, sourceInline);
}
else
{
var p = new Paragraph();
destDocument.Blocks.InsertAfter(destStart.Paragraph, p);
p.Inlines.Add(sourceInline);
}
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
}
else
{
var sourceBlock = sourceStart.Parent as Block;
field.SetValue(sourceBlock, null);
destDocument.Blocks.InsertAfter(destStart.Paragraph, sourceBlock);
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
}
}
}
}
}
And the question - is there an existing solution for custom Copy+Paste code for FlowDocument using XamlReader and XamlWriter?
How to fix the code above so it won't complain about different FlowDocument object or work around this limitation?
EDIT: As an experiment I implemented 2) so that objects can be moved from one FlowDocument to another. The code above is updated - all references to the "field" variable.
It seems the bounty period is about to expire and I made a breakthrough how to implement the above problem so I will share it here.
First of all TextRange.Save has a "preserveTextElements" argument which can be used to serialize InlineUIContainer and BlockUIContainer elements. Also, both these controls are not sealed so can be used as base classes for a custom TextElement implementation.
With the above in mind:
I created an InlineMedia element inherited from InlineUIContainer which serializes it's Child "manually" into a "ChildSource" dependency property using XamlReader and XamlWriter and hides the original "Child" from default serializer
I changed the above implementation of CustomRichTextBox to copy selection using range.Save(ms, DataFormats.Xaml, true).
As you can notice, no special Paste handling is necessary as Xaml is nicely deserialized after swapping original Xaml in the clipboard and this means dragging works as copy from all CustomRichtextBox controls and Paste works even into normal RichTextBox.
The only limitation is that for all InlineMedia controls the ChildSource property need to be updated by serializing it's Child before serializing a whole document and I found no way to do it automatically (hook into TextRange.Save before element is saved).
I can live with that but a nicer solution without this problem will still get a bounty!
InlineMedia element code:
public class InlineMedia: InlineUIContainer
{
public InlineMedia()
{
}
public InlineMedia(UIElement childUIElement) : base(childUIElement)
{
UpdateChildSource();
}
public InlineMedia(UIElement childUIElement, TextPointer insertPosition)
: base(childUIElement, insertPosition)
{
UpdateChildSource();
}
public static readonly DependencyProperty ChildSourceProperty = DependencyProperty.Register
(
"ChildSource",
typeof(string),
typeof(InlineMedia),
new FrameworkPropertyMetadata(null, OnChildSourceChanged));
public string ChildSource
{
get
{
return (string)GetValue(ChildSourceProperty);
}
set
{
SetValue(ChildSourceProperty, value);
}
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new UIElement Child
{
get
{
return base.Child;
}
set
{
base.Child = value;
UpdateChildSource();
}
}
public void UpdateChildSource()
{
IsInternalChildSourceChange = true;
try
{
ChildSource = Save();
}
finally
{
IsInternalChildSourceChange = false;
}
}
public string Save()
{
if(Child == null)
{
return null;
}
using(var stream = new MemoryStream())
{
XamlWriter.Save(Child, stream);
stream.Position = 0;
using(var reader = new StreamReader(stream, Encoding.UTF8))
{
return reader.ReadToEnd();
}
}
}
public void Load(string sourceData)
{
if(string.IsNullOrEmpty(sourceData))
{
base.Child = null;
}
else
{
using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(sourceData)))
{
var child = XamlReader.Load(stream);
base.Child = (UIElement)child;
}
}
}
private static void OnChildSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var img = (InlineMedia) sender;
if(img != null && !img.IsInternalChildSourceChange)
{
img.Load((string)e.NewValue);
}
}
protected bool IsInternalChildSourceChange { get; private set; }
}
CustomRichTextBox control code:
public class CustomRichTextBox: RichTextBox
{
public CustomRichTextBox()
{
DataObject.AddCopyingHandler(this, OnCopy);
}
private void OnCopy(object sender, DataObjectCopyingEventArgs e)
{
if(e.DataObject != null)
{
UpdateDocument();
var range = new TextRange(Selection.Start, Selection.End);
using(var ms = new MemoryStream())
{
range.Save(ms, DataFormats.Xaml, true);
ms.Position = 0;
using(var reader = new StreamReader(ms, Encoding.UTF8))
{
var xaml = reader.ReadToEnd();
e.DataObject.SetData(DataFormats.Xaml, xaml);
}
}
e.Handled = true;
}
}
public void UpdateDocument()
{
ObjectHelper.ExecuteRecursive<InlineMedia>(Document, i => i.UpdateChildSource(), FlowDocumentVisitors);
}
private static readonly Func<object, object>[] FlowDocumentVisitors =
{
x => (x is FlowDocument) ? ((FlowDocument) x).Blocks : null,
x => (x is Section) ? ((Section) x).Blocks : null,
x => (x is BlockUIContainer) ? ((BlockUIContainer) x).Child : null,
x => (x is InlineUIContainer) ? ((InlineUIContainer) x).Child : null,
x => (x is Span) ? ((Span) x).Inlines : null,
x => (x is Paragraph) ? ((Paragraph) x).Inlines : null,
x => (x is Table) ? ((Table) x).RowGroups : null,
x => (x is Table) ? ((Table) x).Columns : null,
x => (x is Table) ? ((Table) x).RowGroups.SelectMany(rg => rg.Rows) : null,
x => (x is Table) ? ((Table) x).RowGroups.SelectMany(rg => rg.Rows).SelectMany(r => r.Cells) : null,
x => (x is TableCell) ? ((TableCell) x).Blocks : null,
x => (x is TableCell) ? ((TableCell) x).BorderBrush : null,
x => (x is List) ? ((List) x).ListItems : null,
x => (x is ListItem) ? ((ListItem) x).Blocks : null
};
}
and finally ObjectHelper class - a visitor helper:
public static class ObjectHelper
{
public static void ExecuteRecursive(object item, Action<object> execute, params Func<object, object>[] childSelectors)
{
ExecuteRecursive<object, object>(item, null, (c, i) => execute(i), childSelectors);
}
public static void ExecuteRecursive<TObject>(object item, Action<TObject> execute, params Func<object, object>[] childSelectors)
{
ExecuteRecursive<object, TObject>(item, null, (c, i) => execute(i), childSelectors);
}
public static void ExecuteRecursive<TContext, TObject>(object item, TContext context, Action<TContext, TObject> execute, params Func<object, object>[] childSelectors)
{
ExecuteRecursive(item, context, (c, i) =>
{
if(i is TObject)
{
execute(c, (TObject)i);
}
}, childSelectors);
}
public static void ExecuteRecursive<TContext>(object item, TContext context, Action<TContext, object> execute, params Func<object, object>[] childSelectors)
{
execute(context, item);
if(item is IEnumerable)
{
foreach(var subItem in item as IEnumerable)
{
ExecuteRecursive(subItem, context, execute, childSelectors);
}
}
if(childSelectors != null)
{
foreach(var subItem in childSelectors.Select(x => x(item)).Where(x => x != null))
{
ExecuteRecursive(subItem, context, execute, childSelectors);
}
}
}
}
1.In current solution a whole FlowDocument object needs to be serialized/deserialized. Performance-wise it should not be a problem
but I need to store information what selection range needs to be
pasted from it (CustomRichTextBoxTag class).
This smells like an opportunity to use an attached property based on the intended behavior you identified. I understand attached properties as a way of adding additional behavior to an element. When you register an attached property, you can add an event handler for when that property value changes.
To take advantage of this, I would wire this attached property to a DataTrigger to update the selection range value for your copy/paste operation.
2.Apparently objects cannot be removed from one document and added to another (a dead-end I discovered recently): 'InlineCollection' element
cannot be inserted in a tree because it is already a child of a tree.
You can get around this by constructing your elements programmatically and also removing your elements programmatically. At the end of the day, you're mainly dealing with either an ItemsControl or a ContentControl. In this case your working with an ItemsControl (i.e. document). As a result just add and remove child elements from your ItemsControl (document) programmatically.