Subscribe Now: Feed Icon

Tuesday, February 19, 2013

ESRI Silverlight MapTip

Today, while working on a sever bug I have checked to see if the bug doesn’t occur in our Silverlight application as well – it doesn’t. But unfortunately (for me) I found another bug: our MapTip didn’t display some of the components names. After fixing the bug in the server I decided to go after the Silverlight one.

On our previous version our name field was named Name but we had another field named NameEng (for the English name). The MapTip simply displayed the Name field to all elements. On our current version we ported our DB to Oracle and decided to have standardized names such as LOCAL_NAME (previously Name) and NAME (previously NameEng). Hence the problem those component without their name just don’t have a field named LOCAL_NAME – and for some strange reason ESRI doesn’t throw an exception of missing key.

Well I tried searching the web but apparently the feature of a fallback value if the first value is null was only implemented in WPF. I have tried using TargetNullValue (crossing my fingers):

<TextBlock Text="{Binding [LOCAL_NAME], TargetNullValue=[NAME]}" FontSize="11" FontWeight="SemiBold"/>

But that just printed [NAME]. I have also tried to use {Binding [NAME]} but that just threw an exception.

I couldn’t change the View Model because the ESRI MapTip works on an inner object in ESRI (which I later found out is an internal sealed class) so adding something like:

<TextBlock Text="{Binding [LOCAL_NAME]}" Visibility="{Binding IsLocal}" FontSize="11" FontWeight="SemiBold"/>
<TextBlock Text="{Binding [NAME]}" Visibility="{Binding IsNotLocal}" FontSize="11" FontWeight="SemiBold"/>

was out.

 

As I see it I had two options:

1. Create a NullToVisibilityConvertor and a NotNullToVisibilityConvertor that when the value is null hides the TextBlock.

public class NullToVisibilityConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value == null ? Visibility.Collapsed : Visibility.Visible;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
    #endregion
}

(the opposite for NotNullToVisibilityConvertor)

<TextBlock Text="{Binding [LOCAL_NAME]}" Visibility="{Binding [LOCAL_NAME], Converter={StaticResource NullToVisibilityConverter}}" FontSize="11" FontWeight="SemiBold"/>
<TextBlock Text="{Binding [NAME]}" Visibility="{Binding [LOCAL_NAME], Converter={StaticResource NotNullToVisibilityConverter}}" FontSize="11" FontWeight="SemiBold"/>

2. Create a Convertor that if one value is null returns the other.

I first tried to use the ConvertorParameter to pass the second value but that came out as simple text. So I have decided to pass the two fields I needed as the ConvertorParameter ('LOCAL_NAME,NAME') and the ESRI class as the binding value:

<TextBlock Text="{Binding ., Converter={StaticResource DictionaryValuePickerConverter}, ConverterParameter='LOCAL_NAME,NAME'}" FontSize="11" FontWeight="SemiBold"/>

Now what I found (through Debugging and JetBrains dotPeek) is that though ESRI passes an internal sealed class named ObservableDictionary (and I cannot cast to it in my convertor) that class implements IDictionary<string, object> so I can just cast the value to that:

public class DictionaryValuePickerConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var dictionary = value as IDictionary<string, object>;
        var stringPrameters = parameter as string;

        if (dictionary == null || stringPrameters == null)
        {
            return String.Empty;
        }
        var parameters = stringPrameters.Split(",".ToCharArray());

        foreach (var key in parameters)
        {
            if (dictionary.ContainsKey(key))
                return dictionary[key];
        }
        return String.Empty;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 

I actually hate both options and had to ask my team leader what he thought. At the end we decided to go with option 2, if somewhere down the line we will need (and God help us if we do) another fallback name field we can just add another using “,YET_ANOTHER_NAME” and it will work…

I hope no one needs this…