Displaying item hierarchy using xaml - c#

I have a object hierarchy created from a class like this:
public class MyTreeItem
{
public MyTreeItem Parent{get;set}
public IList<MyTreeItem> Children{get;set;}
public string Description{get;set;}
//Other properties.
}
Suppose I have an object tree of MyTreeItems such as the following:
A
/ \
B C
/\ \
D E F
Within my program, I will acquire some object in the above hierarchy from a DB query. This item will belong to the second or third level of the tree (B, C, D, E or F in this case). I want to display the full tree path of the acquired object using the Parent property (Kind of like how the path is shown in Windows Explorer.). Suppose I selected the item B, then the path should be displayed as A -> B. If I selected E, the path should be A -> B -> E. Is there a way to get this done in XAML?

so if you are wanting to show this as a string, then all you need to do is design a property that returns the breadcrumb trail based on the parent...
Note: This assumes the 'A' and 'B' are the Description property...
public string Breadcrumb
{
get
{
string breadcrumb = Description;
if(Parent === null)
return breadcrumb;
for(MyTreeItem currentItem = Parent; currentItem != null ; currentItem = currentItem.Parent)
{
breadcrumb = string.Format("{0) -> {1}", currentItem.Description, breadcrumb);
}
return breadcrumb;
}
}
basically the logic is to keep adding the parent's descirption (formatted with the ->) on the front of the current breadcrumb, the for loop will then set the currentItem to the Parent (which should be null at the top level)
e.g for Node E it will build the breadcrumb as such:
Loop 0: E
Loop 1: B -> E
Loop 2: A -> B -> E

How about using the IValueConverter and bind it to the TextBlock text DP.
public class MyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string hierarchy = String.Empty;
if(value != DependencyProperty.UnsetValue)
{
MyTreeItem item = value;
hierarchy = value.Description;
MyTreeItem parentItem = item.Parent;
while(parentItem != null)
{
hierarchy = string.Format("{0) -> {1}", parentItem.Description,
hierarchy);
parentItem = parentItem.Parent;
}
}
return hierarchy;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Here goes your XAML -
<TextBlock Text="{Binding SelectedItem,
Converter={StaticResource MyConverter}}"/>

Related

How to put spaces on a bound enum title on picker

I am attaching enum to a picker and onSelect i am binding to the actual value of the enum, not its title.
My enum is as follows:
public enum Reason
{
AnnualLeave = 12,
Emergency = 23,
MaternityLeave = 34
}
My class uses the following to bind the enum title to the picker
public Reason ReasonSelectedOption { get; set; }
public ObservableCollection<Reason> ReasonDisplay
{
get => new ObservableCollection<Reason>(Enum.GetValues(typeof(Reason)).OfType<Reason>().ToList());
}
The actual picker
<Picker
ItemsSource="{Binding ReasonDisplay}"
SelectedItem="{Binding ReasonSelectedOption}"
Title="Please Select"
HorizontalOptions="FillAndExpand" />
Everything works fine except in the actual picker, the options appear as AnnualLeave and MaternityLeave which is what's expected from my code but i want them to appear as Annual Leave and Maternity Leave (with the space inbetween) preserving the selecteditem value
Current case: When user selects AnnualLeave, selectedItem value is 12, if i convert to string the selected value becomes 0.
I am simply asking how to put spaces inbetween the enum options and also preserve the SelectedItem integer value
Here you have to keep in mind the internationalisation.
Even if you don't have localised texts now, you may have to support it in the future. So, keeping that in mind, you won't need simply to "split" the string, but to take a specific text from somewhere (i.e. translate it according to the culture).
You can achieve it with the help of some attributes, extension methods and some clever binding.
Let's say that you want to have a picker with 2 options - what is the property type. The PropertyType is an enum, that looks like this:
public enum PropertyType
{
House,
Apartment
}
Since the built-in Description attribute can't translate texts for us, we can use a custom attribute to assign a specific text to an enum type, like this:
public enum PropertyType
{
[LocalizedDescription(nameof(R.SingleFamilyHouse))]
House,
[LocalizedDescription(nameof(R.ApartmentBuilding))]
Apartment
}
The attribute code looks like this:
public class LocalizedDescriptionAttribute : DescriptionAttribute
{
private readonly ResourceManager resourceManager;
private readonly string resourceKey;
public LocalizedDescriptionAttribute(string resourceKey, Type resourceType = null)
{
this.resourceKey = resourceKey;
if (resourceType == null)
{
resourceType = typeof(R);
}
resourceManager = new ResourceManager(resourceType);
}
public override string Description
{
get
{
string description = resourceManager.GetString(resourceKey);
return string.IsNullOrWhiteSpace(description) ? $"[[{resourceKey}]]" : description;
}
}
}
R is my resx file. I have created a Resources folder and inside it I have 2 resx files - R.resx (for English strings) & R.de.resx (for German translation). If you don't want to have internationalisation now, you can change the implementation to get your strings from another place. But it is considered a good practice to always use a resx file, even if you only have 1 language. You never now what tomorrow may bring.
Here is my structure:
The idea behind LocalizedDescriptionAttribute class is that the built-in Description attribute isn't very useful for our case. So we'll have to take the resource key that we have provided it, translates and to override the Description attribute, which later we will reference.
Now we need to obtain the localised description text with this helper method:
public static class EnumExtensions
{
public static string GetLocalizedDescriptionFromEnumValue(this Enum value)
{
return !(value.GetType()
.GetField(value.ToString())
.GetCustomAttributes(typeof(LocalizedDescriptionAttribute), false)
.SingleOrDefault() is LocalizedDescriptionAttribute attribute) ? value.ToString() : attribute.Description;
}
}
Now, when we create the bindings for the Picker, we won't just use a simple Enum, but a specific PropertyTypeViewModel, which will have 2 properties - the Enum itself and a Name that will be displayed.
public class PropertyTypeViewModel : BaseViewModel
{
private string name;
public string Name
{
get => name;
set => SetValue(ref name, value);
}
private PropertyType type;
public PropertyType Type
{
get => type;
set => SetValue(ref type, value);
}
public PropertyTypeViewModel()
{
}
public PropertyTypeViewModel(PropertyType type)
: this()
{
Type = type;
Name = type.GetLocalizedDescriptionFromEnumValue();
}
}
The important line is the last one - Name = type.GetLocalizedDescriptionFromEnumValue();
The final thing that is left is to set your Picker's ItemsSource to your collection of PropertyTypeViewModels and ItemDisplayBinding to be pointing to the Name property - ItemDisplayBinding="{Binding Name}"
That's it - now you have a Picker with dynamic localised strings.
You could use a converter
public class EnumToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) return null;
var valueAsString = value.ToString();
valueAsString = valueAsString.SplitCamelCase();
return valueAsString;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And for the SplitCamelCase I wrote this but I'm sure there are cleaner options:
public static string SplitCamelCase(this string str)
{
string result = "";
for (int i = 0; i < str.Count(); i++)
{
var letter = str[i];
var previousLetter = i != 0 ? str[i - 1] : 'A';
if (i != 0 && char.IsUpper(previousLetter) == false && char.IsUpper(letter)) result = result + " " + letter;
else result = result + letter;
}
return result;
}
Then just used it like so:
<TextBlock Text="{Binding Converter={StaticResource EnumToStringConverter}}"/>

How To Identify A StripLine Within The StripLines Collection (Axis) (Serialization)?

I am writing a sort of MsCharts designer.
- Design Chart, ChartAreas, Series,...
- The object is saved via the standard System.Windows.Forms.DataVisualization.Charting.Chart.ChartSerializer
I want the user to be able to add multiple Striplines to the Axis's.
I am attempting to identify a StripLine within the StripLines Collection of an Axis.
The Name property of a StripLine is read only (get, no set).
I see no way to actually set the Name property.
I do not understand how this is useful?
I was going to use the Tag property of the StripLine but alas the Tag property is not serialized.
Note:
If I edit the serialized chart and add Tag="AStripLine" to a element and then load it via Chart.ChartSerializer The Tag= value is in fact there.
If I save / serialize the chart via Chart.ChartSerializer Tag is not saved.
Any help / ideas would be greatly appreciated.
Tag property is of type of object and it's decorated with an internal attribute telling the serializer to not serialize Tag property. So the behavior is expected.
But since the serializer relies on TypeDescriptor, you can create a new TypeDescriptor for StripLine class describe Tag property in a different way, for example:
Make it browsable in property grid
Make it editable in property grid
Make it serializable for chart serializer
So it serializes and deserializes it correctly with this format, for example:
<StripLine Text="text1" Tag="1" />
Also shows it in property grid at run-time:
You need to create the following classes:
StripLineTypeDescriptionProvider: Helps to register a new type descriptor for StripLine
StripLineTypeDescriptor: Describes the properties of the type and allows you to change Tag property behavior. In this class we override GetProperties and replace the Tag property with a modified property descriptor which tells the serializer to serialize Tag and also tells the property grid to show it and make it editable.
MyPropertyDescriptor: Helps us to specify the new type of Tag property. You can decide to set it as string, int or even a complex type. It's enough the type be convertible to and from string.
Then it's enough to register the type descriptor for StripLine in constructor or load event of the form:
var provider = new StripLineTypeDescriptionProvider();
TypeDescriptor.AddProvider(provider, typeof(StripLine));
Implementations
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
public class StripLineTypeDescriptionProvider : TypeDescriptionProvider
{
public StripLineTypeDescriptionProvider()
: base(TypeDescriptor.GetProvider(typeof(object))) { }
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
ICustomTypeDescriptor baseDescriptor = base.GetTypeDescriptor(objectType, instance);
return new StripLineTypeDescriptor(baseDescriptor);
}
}
public class StripLineTypeDescriptor : CustomTypeDescriptor
{
ICustomTypeDescriptor original;
public StripLineTypeDescriptor(ICustomTypeDescriptor originalDescriptor)
: base(originalDescriptor)
{
original = originalDescriptor;
}
public override PropertyDescriptorCollection GetProperties()
{
return this.GetProperties(new Attribute[] { });
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
var properties = base.GetProperties(attributes).Cast<PropertyDescriptor>().ToList();
var tag = properties.Where(x => x.Name == "Tag").FirstOrDefault();
var tagAttributes = tag.Attributes.Cast<Attribute>()
.Where(x => x.GetType() != typeof(BrowsableAttribute)).ToList();
var serializationAttribute = tagAttributes.Single(
x => x.GetType().FullName == "System.Windows.Forms.DataVisualization.Charting.Utilities.SerializationVisibilityAttribute");
var visibility = serializationAttribute.GetType().GetField("_visibility",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
visibility.SetValue(serializationAttribute, Enum.Parse(visibility.FieldType, "Attribute"));
tagAttributes.Add(new BrowsableAttribute(true));
var newTag = new MyPropertyDescriptor(tag, tagAttributes.ToArray());
properties.Remove(tag);
properties.Add(newTag);
return new PropertyDescriptorCollection(properties.ToArray());
}
}
public class MyPropertyDescriptor : PropertyDescriptor
{
PropertyDescriptor o;
public MyPropertyDescriptor(PropertyDescriptor originalProperty,
Attribute[] attributes) : base(originalProperty)
{
o = originalProperty;
AttributeArray = attributes;
}
public override bool CanResetValue(object component)
{ return o.CanResetValue(component); }
public override object GetValue(object component) => o.GetValue(component);
public override void ResetValue(object component) { o.ResetValue(component); }
public override void SetValue(object component, object value) { o.SetValue(component, value); }
public override bool ShouldSerializeValue(object component) => true;
public override AttributeCollection Attributes => new AttributeCollection(AttributeArray);
public override Type ComponentType => o.ComponentType;
public override bool IsReadOnly => false;
public override Type PropertyType => typeof(string);
}
References
Here are source code for classes which will help you to learn how the chart serialization works:
ChartElement
ChartSerializer
SerializerBase
XmlFormatSerializer
This is truely a weird finding!
At first I thought that maybe the Names would be autogenerated in a useful way, say StripLine1, StripLine2 etc.
But they all get StripLine as their Name.
So it will be useless for your purpose of identifiying them.
There is however the Tag property that comes to the rescue; it easy to e.g. set to a unique string..:
StripLine sl = new StripLine()
{ Text = "LW" , StripWidth = 2, ForeColor = Color.Teal, Tag = "Low-Water"};
To make it unique for an axis AxisY could use this:
StripLine sl = new StripLine()
{ Text = "LW" , StripWidth = 2, ForeColor = Color.Teal,
Tag = "Low-Water" + chart1.ChartAreas[0].AxisY.StripLines.Count };
As Tag is of type object you could create a class to hold more info, like a shortname and a description..
Update: I just noticed that you know about Tags and how they are not serialized. You could however use this workaround:
Before Serializing you loop over all StripLines and change the Text to : oldText + separator + Tag string.
After de-serializing you do the reverse.
As separator you could use the tab (\t) or other characters (or strings) you don't expect in the texts.. (The vertical tab, my original idea is not an allowed xml entity..)
Here is a function to prepare the Text and to reconstruct the Tags:
void StripLineTagger(Chart chart, bool beforeSer)
{
char sep = '\t';
var axes = new List<Axis> { chart.ChartAreas[0].AxisX, chart.ChartAreas[0].AxisX2,
chart.ChartAreas[0].AxisY, chart.ChartAreas[0].AxisY2};
foreach (var ax in axes)
foreach (var sl in ax.StripLines)
{
if (beforeSer) sl.Text = sl.Text + sep + sl.Tag.ToString();
else
{
var p = sl.Text.Split(sep);
sl.Text = p[0];
sl.Tag = p[1];
}
}
}
This is untested and lacks all checks..!
Update 2:
You could add a subclass of your own to replace the regular StripLines:
class MyStripLine : StripLine
{
new public string Name { get; set; } // looks fine butwon't get serialized
public string ID{ get; set; } // gets serialized
//..
public MyStripLine()
{
}
}
They can be added to the StripLines collections and work as expected. Unfortunately the Name property only looks good but doesn't get written out.. an while using another property (ID) is simple I can't get de-serialization to work.

My custom value converter causes XAML validation tool to fail

I've created a custom converter that performs converting of values based on configured mapping. It looks like below
public class UniversalConverter : List<ConverterItem>, IValueConverter
{
private bool useDefaultValue;
private object defaultValue;
public object DefaultValue
{
get { return defaultValue; }
set
{
defaultValue = value;
useDefaultValue = true;
}
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
foreach (var item in this)
if (Equals(item.From, value))
return item.To;
if (useDefaultValue)
return DefaultValue;
throw new ConversionException(string.Format("Value {0} can't be converted and default value is not allowed", value));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
foreach (var item in this)
if (Equals(item.To, value))
return item.From;
throw new ConversionException(string.Format("Value {0} can't be converted back", value));
}
}
public class ConverterItem
{
public object From { get; set; }
public object To { get; set; }
}
public class ConversionException : Exception
{
public ConversionException() { }
public ConversionException(string message) : base(message) { }
}
Sample XAML is below
<core:UniversalConverter x:Key="ItemCountToVisiblityConverter" DefaultValue="{x:Static Visibility.Collapsed}">
<core:ConverterItem To="{x:Static Visibility.Visible}">
<core:ConverterItem.From>
<system:Int32>0</system:Int32>
</core:ConverterItem.From>
</core:ConverterItem>
</core:UniversalConverter>
Now everything builds and works fine, but if I use it XAML Visual Studio underscores the whole file with curvy blue lines and shows two kind of mistakes:
1) If converter is put into ResourceDictionary AND is assigned an x:Key attribute it shows Missing key value on 'UniversalConverter' object
2) If I assign DefaultValue property any value (e.g {x:Null}) the message is XAML Node Stream: Missing EndMember for 'StuffLib.UniversalConverter.{http://schemas.microsoft.com/winfx/2006/xaml}_Items' before StartMember 'StuffLib.UniversalConverter.DefaultValue'
What is the reason for those messages? I can live with them but they hide all other compiler and ReSharper markings
Don't inherit from list, just create Items property in your converter:
[ContentProperty("Items")]
public class UniversalConverter : IValueConverter
{
public ConverterItem[] Items { get; set; }
public object DefaultValue { get; set; }
//all other stuff goes here
}
and xaml:
<l:UniversalConverter x:Key="MyConverter">
<x:Array Type="l:ConverterItem">
<l:ConverterItem From="..." To="..." />
Based on answer given by #Leiro
[ContentProperty("Items")]
public class UniversalConverter : IValueConverter
{
public UniversalConverter()
{
Items = new List<ConverterItem>();
}
public List<ConverterItem> Items { get; private set; }
//All other logic is the same
}
Note that this way you won't need to wrap items in collection in XAML
Resulting XAML
<core:UniversalConverter x:Key="ItemCountToVisiblityConverter" DefaultValue="{x:Static Visibility.Collapsed}">
<core:ConverterItem To="{x:Static Visibility.Visible}">
<core:ConverterItem.From>
<system:Int32>0</system:Int32>
</core:ConverterItem.From>
</core:ConverterItem>
</core:UniversalConverter>
It's because it is being used at design time but there is no data so I suspect a NullReferenceException is being thrown. Try checking for design time mode as follows at the top of the IValueConverter.Convert() method body:
// Check for design mode.
if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
{
return false;
}

How to bind and ConvertBack separated TextBox strings to an ObservableCollection<string>?

I have view-model with ObservableCollection<string> that's bound to a TextBox. Users must be able to enter textual data with some separators (say a comma or a semicolon), and have the change reflected to the ObservableCollection<string>, so if I type in the box abc,123,one, the ObservableCollection<string> will have three items: abc, 123 and one.
My examplary ViewModel:
class ViewModel {
public ObservableCollection<string> MyObservableCollection { get; set; }
}
My TextBox:
<TextBox Text="{Binding MyObservableCollection, Converter={StaticResource Conv}, Mode=TwoWay}"/>
My value-converter:
public class ObservableCollectionToAndFromString : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<string> oc;
if (targetType != typeof (string) || (oc = value as ObservableCollection<string>) == null)
return Binding.DoNothing;
return string.Join(",", oc);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var str = value as string;
return str == null
? Binding.DoNothing
: str.Split(',');
}
}
But it doesn't work from understandable reason - WPF doesn't know how to replace an ObservableCollection<string> with string[].
But I don't want the framework to replace my ObservableCollection<string> object with new ObservableCollection<string> object (in that case I could have just created new one and return it in the ConvertBack method). I want it to add/remove items to/from the existing ObservableCollection<string>.
How can I accomplish this (without another model-view string property that will do the work in code-behind)?
Edit:
Ok, I give up... movel-view is the way to go... I hoped to make it work on several fields in one time, but that just to awkward.
I think it's better to add a string representation of data to your viewmodel.
internal class ViewModel
{
public ObservableCollection<string> MyObservableCollection { get; set; }
public string MyString
{
get { return string.Join(",", MyObservableCollection); }
set { // update observable collection here based on value }
}
}
<TextBox Text="{Binding MyString, Mode=TwoWay}"/>

XAML Binding to complex value objects

I have a complex value object class that has 1) a number or read-only properties; 2) a private constructor; and 3) a number of static singleton instance properties [so the properties of a ComplexValueObject never change and an individual value is instantiated once in the application's lifecycle].
public class ComplexValueClass
{
/* A number of read only properties */
private readonly string _propertyOne;
public string PropertyOne
{
get
{
return _propertyOne;
}
}
private readonly string _propertyTwo;
public string PropertyTwo
{
get
{
return _propertyTwo;
}
}
/* a private constructor */
private ComplexValueClass(string propertyOne, string propertyTwo)
{
_propertyOne = propertyOne;
_propertyTwo = PropertyTwo;
}
/* a number of singleton instances */
private static ComplexValueClass _complexValueObjectOne;
public static ComplexValueClass ComplexValueObjectOne
{
get
{
if (_complexValueObjectOne == null)
{
_complexValueObjectOne = new ComplexValueClass("string one", "string two");
}
return _complexValueObjectOne;
}
}
private static ComplexValueClass _complexValueObjectTwo;
public static ComplexValueClass ComplexValueObjectTwo
{
get
{
if (_complexValueObjectTwo == null)
{
_complexValueObjectTwo = new ComplexValueClass("string three", "string four");
}
return _complexValueObjectTwo;
}
}
}
I have a data context class that looks something like this:
public class DataContextClass : INotifyPropertyChanged
{
private ComplexValueClass _complexValueClass;
public ComplexValueClass ComplexValueObject
{
get
{
return _complexValueClass;
}
set
{
_complexValueClass = value;
PropertyChanged(this, new PropertyChangedEventArgs("ComplexValueObject"));
}
}
}
I would like to write a XAML binding statement to a property on my complex value object that updates the UI whenever the entire complex value object changes. What is the best and/or most concise way of doing this? I have something like:
<Object Value="{Binding ComplexValueObject.PropertyOne}" />
but the UI does not update when ComplexValueObject as a whole changes.
Your original scenario should work just fine because in most cases Bindings recognize change notifications on any part of their property path. I in fact tried out the code you posted to confirm and it does work just fine.
Are there other complexities you may not be expressing in your stripped down sample? The primary one I can think of would be collections->ItemsSource Bindings but there could be something related to the property you're assigning the bound value to (since it's obviously not an Object) or something else entirely.
You don't notify on changes to PropertyOne so UI will not update. Instead bind to ComplexValueObject and use value converter to get the property value.
<Object Value="{Binding Path=ComplexValueObject, Converter={StaticResource ComplexValueConverter}, ConverterParameter=PropertyOne}" />
public class ComplexValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ComplexValue cv = value as ComplexValue;
string propName = parameter as string;
switch (propName)
{
case "PropertyOne":
return cv.PropertyOne;
case "PropertyTwo":
return cv.PropertyTwo;
default:
throw new Exception();
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
You need INotifyPropertyChanged on your Complex class. The only notifies if you reassign the entire property in the parent, the properties of that child class need to notify to0 if you are going to bind to them.

Categories