DevExpress WPF Components

ExtendedLookUpEditor.xaml

<UserControl x:Class="Views.Controls.ExtendedLookUpEditor"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
             x:Name="root">

    <dxg:LookUpEdit x:Name="editor"
                    MinWidth="0"
                    AutoPopulateColumns="False"
                    ValueMember="{Binding ElementName=root, Path=ValueMember}"
                    DisplayMember="{Binding ElementName=root, Path=DisplayMember}"
                    ItemsSource="{Binding ElementName=root, Path=ItemsSource}"
                    EditValue="{Binding ElementName=root, Path=EditValue, Mode=TwoWay}">
        <dxg:LookUpEdit.StyleSettings>
            <dxg:SearchLookUpEditStyleSettings AllowGrouping="False"/>
        </dxg:LookUpEdit.StyleSettings>
        <dxg:LookUpEdit.PopupContentTemplate>
            <ControlTemplate>
                <dxg:GridControl Name="PART_GridControl"
                                 ColumnsSource="{Binding ElementName=root, Path=PopupColumns}"
                                 AutoGenerateColumns="None">
                    <dxg:GridControl.View>
                        <dxg:TableView AutoWidth="True"
                                       ShowGroupPanel="False"
                                       EnableImmediatePosting="True"/>
                    </dxg:GridControl.View>
                </dxg:GridControl>
            </ControlTemplate>
        </dxg:LookUpEdit.PopupContentTemplate>
    </dxg:LookUpEdit>

</UserControl>

ExtendedLookUpEditor.xaml.cs

using DevExpress.Xpf.Editors;
using DevExpress.Xpf.Grid;
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace Views.Controls
{
    public partial class ExtendedLookUpEditor : UserControl
    {
        public static readonly DependencyProperty EditValueProperty = DependencyProperty.Register(
            nameof(EditValue),
            typeof(object),
            typeof(ExtendedLookUpEditor),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
            nameof(ItemsSource),
            typeof(IEnumerable),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(null));

        public static readonly DependencyProperty ValueMemberProperty = DependencyProperty.Register(
            nameof(ValueMember),
            typeof(string),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(null, OnColumnsConfigChanged));

        public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.Register(
            nameof(DisplayMember),
            typeof(string),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(null, OnColumnsConfigChanged));

        public static readonly DependencyProperty HideValueColumnProperty = DependencyProperty.Register(
            nameof(HideValueColumn),
            typeof(bool),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(false, OnColumnsConfigChanged));

        public static readonly DependencyProperty ValueColumnHeaderProperty = DependencyProperty.Register(
            nameof(ValueColumnHeader),
            typeof(string),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(null, OnColumnsConfigChanged));

        public static readonly DependencyProperty DisplayColumnHeaderProperty = DependencyProperty.Register(
            nameof(DisplayColumnHeader),
            typeof(string),
            typeof(ExtendedLookUpEditor),
            new PropertyMetadata(null, OnColumnsConfigChanged));

        public ObservableCollection<GridColumn> PopupColumns { get; } = new ObservableCollection<GridColumn>();

        /// <summary>Wordt gevuurd wanneer de geselecteerde waarde verandert.</summary>
        public event EditValueChangedEventHandler EditValueChanged;

        public ExtendedLookUpEditor()
        {
            InitializeComponent();
            editor.EditValueChanged += (s, e) => EditValueChanged?.Invoke(this, e);
        }

        /// <summary>Het geselecteerde sleutelwaarde (gebonden aan ValueMember).</summary>
        public object EditValue
        {
            get => GetValue(EditValueProperty);
            set => SetValue(EditValueProperty, value);
        }

        /// <summary>De lijst met keuze-opties.</summary>
        public IEnumerable ItemsSource
        {
            get => (IEnumerable)GetValue(ItemsSourceProperty);
            set => SetValue(ItemsSourceProperty, value);
        }

        /// <summary>De property naam van het Id/sleutelveld in de opties (bijv. "Code").</summary>
        public string ValueMember
        {
            get => (string)GetValue(ValueMemberProperty);
            set => SetValue(ValueMemberProperty, value);
        }

        /// <summary>De property naam van het weergaveveld in de opties (bijv. "Description").</summary>
        public string DisplayMember
        {
            get => (string)GetValue(DisplayMemberProperty);
            set => SetValue(DisplayMemberProperty, value);
        }

        /// <summary>Verberg de ValueMember kolom in de popup (default: false).</summary>
        public bool HideValueColumn
        {
            get => (bool)GetValue(HideValueColumnProperty);
            set => SetValue(HideValueColumnProperty, value);
        }

        /// <summary>Kolomtitel voor de ValueMember kolom. Standaard de property naam.</summary>
        public string ValueColumnHeader
        {
            get => (string)GetValue(ValueColumnHeaderProperty);
            set => SetValue(ValueColumnHeaderProperty, value);
        }

        /// <summary>Kolomtitel voor de DisplayMember kolom. Standaard de property naam.</summary>
        public string DisplayColumnHeader
        {
            get => (string)GetValue(DisplayColumnHeaderProperty);
            set => SetValue(DisplayColumnHeaderProperty, value);
        }

        private static void OnColumnsConfigChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            => ((ExtendedLookUpEditor)d).RebuildColumns();

        private void RebuildColumns()
        {
            PopupColumns.Clear();

            var valueMember = ValueMember;
            var displayMember = DisplayMember;

            if (string.IsNullOrEmpty(valueMember) && string.IsNullOrEmpty(displayMember))
                return;

            if (!string.IsNullOrEmpty(valueMember) && !HideValueColumn)
            {
                PopupColumns.Add(new GridColumn
                {
                    FieldName = valueMember,
                    Header = ValueColumnHeader ?? valueMember,
                    Width = 100
                });
            }

            if (!string.IsNullOrEmpty(displayMember))
            {
                PopupColumns.Add(new GridColumn
                {
                    FieldName = displayMember,
                    Header = DisplayColumnHeader ?? displayMember,
                    Width = 200
                });
            }
        }
    }
}

GeoCoordinatesEditor.xaml

<UserControl x:Class="Views.Controls.GeoCoordinatesEditor"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
             xmlns:domain="clr-namespace:Domain"
             x:Name="root">

    <UserControl.Resources>
        <domain:GeoCoordinatesDisplayConverter x:Key="GeoDisplayConverter" DisplayDecimals="5"/>
    </UserControl.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <dxe:TextEdit Grid.Column="0"
                      x:Name="editor"
                      EditValue="{Binding ElementName=root, Path=Coordinates, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                      DisplayTextConverter="{StaticResource GeoDisplayConverter}"/>

        <!-- Kopieer naar klembord (afgerond op 7 decimalen) -->
        <Button Grid.Column="1"
                Width="24" Height="24"
                Margin="2,0,0,0"
                VerticalAlignment="Center"
                ToolTip="Coördinaten kopiëren naar klembord"
                Click="CopyButton_Click"
                Padding="0"
                Focusable="False">
            <TextBlock Text="📋" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        </Button>

        <!-- Openen in Google Maps -->
        <Button Grid.Column="2"
                Width="24" Height="24"
                Margin="2,0,0,0"
                VerticalAlignment="Center"
                ToolTip="Openen in Google Maps"
                Click="MapsButton_Click"
                Padding="0"
                Focusable="False">
            <TextBlock Text="🗺" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        </Button>
    </Grid>

</UserControl>

GeoCoordinatesEditor.xaml.cs

using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using Domain;

namespace Views.Controls
{
    public partial class GeoCoordinatesEditor : UserControl
    {
        public static readonly DependencyProperty CoordinatesProperty = DependencyProperty.Register(
            nameof(Coordinates),
            typeof(string),
            typeof(GeoCoordinatesEditor),
            new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public GeoCoordinatesEditor()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Coördinatenstring in Google Maps formaat, bijv. "51.83082564125974, 4.585666568317119".
        /// Gebruik <see cref="Domain.GeoCoordinatesHelper"/> om de afzonderlijke lat/lon doubles op te halen.
        /// </summary>
        public string Coordinates
        {
            get => (string)GetValue(CoordinatesProperty);
            set => SetValue(CoordinatesProperty, value);
        }

        private void CopyButton_Click(object sender, RoutedEventArgs e)
        {
            if (GeoCoordinates.TryParse(Coordinates, out var coords))
                Clipboard.SetText(coords.ToString());
        }

        private void MapsButton_Click(object sender, RoutedEventArgs e)
        {
            if (GeoCoordinates.TryParse(Coordinates, out var coords))
            {
                var url = string.Format(
                    System.Globalization.CultureInfo.InvariantCulture,
                    "https://www.google.com/maps/@{0},{1},500m",
                    coords.Latitude, coords.Longitude);
                Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
            }
        }
    }
}

using System;
using System.Globalization;
using System.Windows.Data;

namespace Domain
{
    /// <summary>
    /// Immutable waardetype dat een geografische coördinaat (lat, lon) vasthoudt.
    /// Kan worden opgeslagen als één string in het formaat dat Google Maps gebruikt:
    /// "51.83082564125974, 4.585666568317119"
    /// </summary>
    public sealed class GeoCoordinates
    {
        private static readonly char[] Separators = { ',', ';' };

        public double Latitude { get; }
        public double Longitude { get; }

        public bool IsEmpty => Latitude == 0d && Longitude == 0d;

        public GeoCoordinates(double latitude, double longitude)
        {
            Latitude = latitude;
            Longitude = longitude;
        }

        /// <summary>
        /// Probeert een coördinatenstring te parsen naar een <see cref="GeoCoordinates"/> instantie.
        /// Accepteert het formaat "lat, lon" of "lat; lon" (decimaalteken punt of komma per getal).
        /// </summary>
        public static bool TryParse(string value, out GeoCoordinates result)
        {
            result = Empty;
            if (string.IsNullOrWhiteSpace(value))
                return false;

            var parts = value.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length < 2)
                return false;

            if (!TryParseDouble(parts[0].Trim(), out var lat) ||
                !TryParseDouble(parts[1].Trim(), out var lon))
                return false;

            result = new GeoCoordinates(lat, lon);
            return true;
        }

        /// <summary>Aantal decimalen voor opslag (≈1cm nauwkeurigheid).</summary>
        public const int StorageDecimals = 7;

        /// <summary>Formatteert als "lat, lon" string afgerond op <see cref="StorageDecimals"/> decimalen (voor opslag).</summary>
        public override string ToString()
        {
            if (IsEmpty) return string.Empty;
            var fmt = "F" + StorageDecimals;
            return string.Format(CultureInfo.InvariantCulture, "{0}, {1}",
                Math.Round(Latitude,  StorageDecimals, MidpointRounding.AwayFromZero).ToString(fmt, CultureInfo.InvariantCulture),
                Math.Round(Longitude, StorageDecimals, MidpointRounding.AwayFromZero).ToString(fmt, CultureInfo.InvariantCulture));
        }

        /// <summary>
        /// Formatteert als leesbare weergavestring afgerond op <paramref name="decimals"/> decimalen.
        /// 5 decimalen ≈ 1m nauwkeurigheid (ruim voldoende voor ~15m GPS nauwkeurigheid).
        /// </summary>
        public string ToDisplayString(int decimals = 5)
        {
            if (IsEmpty) return string.Empty;
            var fmt = "F" + decimals;
            return string.Format(CultureInfo.InvariantCulture, "{0}, {1}",
                Math.Round(Latitude,  decimals, MidpointRounding.AwayFromZero).ToString(fmt, CultureInfo.InvariantCulture),
                Math.Round(Longitude, decimals, MidpointRounding.AwayFromZero).ToString(fmt, CultureInfo.InvariantCulture));
        }

        public static readonly GeoCoordinates Empty = new GeoCoordinates(0d, 0d);

        private static bool TryParseDouble(string s, out double value)
        {
            return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value)
                || double.TryParse(s, NumberStyles.Float, CultureInfo.CurrentCulture, out value);
        }
    }

    /// <summary>Helper-methoden voor het werken met coördinaten als string property.</summary>
    public static class GeoCoordinatesHelper
    {
        /// <summary>Leest een <see cref="GeoCoordinates"/> uit een coördinatenstring. Geeft <see cref="GeoCoordinates.Empty"/> terug als de string leeg of ongeldig is.</summary>
        public static GeoCoordinates Parse(string coordinatesString)
            => GeoCoordinates.TryParse(coordinatesString, out var result) ? result : GeoCoordinates.Empty;

        /// <summary>Formatteert een <see cref="GeoCoordinates"/> naar een string met volledige precisie.</summary>
        public static string Format(GeoCoordinates coordinates)
            => coordinates?.ToString() ?? string.Empty;

        /// <summary>Formatteert losse lat/lon doubles naar een coördinatenstring met volledige precisie.</summary>
        public static string Format(double latitude, double longitude)
            => Format(new GeoCoordinates(latitude, longitude));
    }

    /// <summary>
    /// WPF IValueConverter die een coördinatenstring omzet naar een afgeronde weergavestring,
    /// bedoeld als DisplayTextConverter voor een DevExpress TextEdit.
    /// EditValue behoudt de volledige precisie-string; de weergave toont 5 decimalen (≈1m, ~15m GPS).
    /// </summary>
    public sealed class GeoCoordinatesDisplayConverter : IValueConverter
    {
        /// <summary>Aantal decimalen voor de weergave. Default 5 (≈1m nauwkeurigheid).</summary>
        public int DisplayDecimals { get; set; } = 5;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var coords = GeoCoordinatesHelper.Parse(value as string);
            return coords.IsEmpty ? string.Empty : coords.ToDisplayString(DisplayDecimals);
        }

        /// <summary>Bij bewerken typt de gebruiker de volledige string; EditValue wordt ongewijzigd doorgegeven.</summary>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            => value;
    }
}

101510cookie-checkDevExpress WPF Components