Track Editor: Difference between revisions
|  (→) |  (→) | ||
| Line 581: | Line 581: | ||
|      return SelectedTrackList != null && SelectedTrack != null; |      return SelectedTrackList != null && SelectedTrack != null; | ||
| } | } | ||
| . . . | . . . | ||
| internal void RefreshTrackDisplay() | |||
| { | |||
|     Tracks = _trackServiceEngine.GetAllTracks(SelectedTrackList, ActiveTrackIdFilter, TrackIdFilter, ActiveTagFilter, TagFilterKey, TagFilterValue); | |||
|     NotifyPropertyChanged(() => NewTrack); | |||
|     NotifyPropertyChanged(() => SelectedTrack); | |||
|     NotifyPropertyChanged(() => Tracks); | |||
|     NotifyPropertyChanged(() => ActiveTrackIdFilter); | |||
|     NotifyPropertyChanged(() => TrackIdFilter); | |||
| } | |||
| . . . | |||
| </source> | </source> | ||
| Line 607: | Line 619: | ||
|          _vm.RefreshTrackDisplay(); |          _vm.RefreshTrackDisplay(); | ||
|      } |      } | ||
| } | } | ||
| . . . | . . . | ||
Revision as of 12:32, 21 October 2019
This section describes how to create a WPF application interacting with a Maria Track Service, without using MariaUserControl and track layer.
General
- Note
- 
- For general description of track related info, see General track service information.
- For this part you will need to include the TPG.Maria.TrackLayer NuGet package as a minimum.
- You also need to have a Track Service available.
- Sample code for this section is the MariaTrackEditor project, in the Sample Projects solution.
 
Start with creating a WPF App project!
- Add a view model class, e.g. TrackEditorViewModel - inheriting ViewModelBase
- Set the view model class to be the DataContext for your application.
Track service engine
The Track service engine encapsulates service interaction.
Available functions:
- Connection
- 
- ConnectToTrackService
 - Connect to specified track service.
- If URI is not given, endpoint info from configuration file will be used (if available).
 - Binding type is assumed to be BasicHttp!
 - For service configuration, see Basic map client, Service configuration
 
 - Disconnect
 - Disconnect from service
 - IsConnected
 - Get current connection status.
 
- Track lists
- 
- GetTrackLists
 - Retreive available tracklists from track service.
 - AddTrackList
 - Create a new track list.
 - RemoveTrackList
 - Remove track list, and all track info, if any.
 
- Tracks
- 
- GetAllTracks
 - Retreive available tracks from a specific track list, matching the search criteria.
 - GetTrackData
 - Retreive specified tracks from a specific track list.
 - AddOrUpdateTrack
 - Create or update specific track.
 
- Track history setting
- 
- GetTrackHistoryOptions
 - SetDefaultTrackHistoryOptions
 - Set default track history options for new track lists.
 - SetTrackHistoryOptions
 - Retreive track history options for specified track list.
 
- Track history
- 
- RemoveTrack
 - Remove specified track from specific track list
 - GetTrackHistory
 - Retreive available track history information for specified track & track list, according to filter criteria.
 
Source code for MariaTrackServiceEngine
Track editor
Connection management
Connection features:
- Connect to track service
- Default track service from configuration
- Alternative track service
 
- Disconnect from service
- Show connection status
At startup, the Track Editor should try to connect to the latest track service successfully connected to. 
To store the last value used, add a string value, StrLastUri to the project settings.
Add the following to your window xaml:
<GroupBox Header="Connection" >
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Label Content="Service URI"/>
        <TextBox Grid.Row="0" Grid.Column="1" Margin="2"
                 Text="{Binding ConnectionUri}"/>
        <Button Grid.Row="0" Grid.Column="3" Height="22" Margin="2" 
                Content="Connect"
                Command="{Binding ConnectCmd}" />
        <Label Grid.Row="1" Grid.Column="0" Margin="2"
               Content="Status"/>
        <TextBlock Grid.Row="1" Grid.Column="1" Margin="2"
                   VerticalAlignment="Center"
                   Text="{Binding ConnectionStatus}"/>
        <Button Grid.Row="1" Grid.Column="3" Height="22" Margin="2"
                Content="Disconnect" 
                Command="{Binding DisconnectCmd}"/>
    </Grid>
</GroupBox>
Then, add the following to your view model:
. . .
public ICommand ConnectCmd { get { return new DelegateCommand(Connect, CanConnect); } }
public ICommand DisconnectCmd { get { return new DelegateCommand(Disconnect, CanDisconnect); } }
. . .
private void Connect(object obj = null)
{
    ConnectionStatus = "Connection requested ... ";
    if (_trackServiceEngine.ConnectToTrackService(ref _connectionUri))
    {
        Settings.Default.StrLastUri = ConnectionUri;
        Settings.Default.Save();
        ConnectionStatus = "Connected!";
    }
    else 
    {
        ConnectionStatus = "Not connected, connection failed! ";
        if (string.IsNullOrWhiteSpace(ConnectionUri))
        {
            ConnectionStatus += "\nSupply URI - or correct 'system.serviceModel' configuration!";
        }
    }
    RefreshConnectionInfo();
}
private bool CanConnect(object obj)
{
    return !_trackServiceEngine.IsConnected();
}
private void Disconnect(object obj)
{
    _trackServiceEngine.Disconnect();
    ConnectionStatus = "Disconnected!";
}
private bool CanDisconnect(object obj)
{
    return  _trackServiceEngine.IsConnected(); 
}
. . .
public string ConnectionUri {
    get { return _connectionUri; }
    set
    {
        _connectionUri = value;
        NotifyPropertyChanged(() => ConnectionUri);
    }
}
public string ConnectionStatus
{
    get { return _connectionStatus; }
    set
    {
        _connectionStatus = value;
        NotifyPropertyChanged(() => ConnectionStatus);
    }
}      
. . .
internal void RefreshConnectionInfo()
{
    NotifyPropertyChanged(() => ConnectionStatus);
    NotifyPropertyChanged(() => ConnectionUri);
}
. . .
Track list management
Track list features:
- Display existing track lists
- Filtered view
 
- Add new track list
- Select track list
- Remove selected track list
Add the following to your window xaml:
<GroupBox Header="Track lists" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        
        <Label Grid.Row="0" Grid.Column="0" Margin="2"
               Content="New list"/>
        <TextBox Grid.Row="0" Grid.Column="1" Margin="2"
                 Text="{Binding NewTrackList}"
                 TextChanged="NewTrackListChanged"/>
        <CheckBox Grid.Row="1" Grid.Column="0" Margin="2"
                  Content="Filter" VerticalAlignment="Center" 
                  IsChecked="{Binding ActiveFilter}" />
        <TextBox Grid.Row="1" Grid.Column="1" Margin="2" 
                 Text="{Binding TrackListFilter}" 
                 VerticalAlignment="Center"
                 TextChanged="FilterChanged" />
        <StackPanel Grid.Row="2" Grid.Column="0" >
            <Label Margin="2"
                   Content="Track lists"/>
            <StackPanel Orientation="Horizontal" Margin="2">
                <Label Content="#"/>
                <Label Content="{Binding TrackLists.Count}"/>
            </StackPanel>
        </StackPanel>
        
        <ListBox Grid.Row="2" Grid.Column="1" Margin="2"
                 VerticalAlignment="Top"
                 ItemsSource="{Binding TrackLists}"
                 SelectedItem="{Binding SelectedTrackList}"/>
        
        <Button Grid.Row="0" Grid.Column="3" Height="22" Margin="2" 
                Content="Add"
                Command="{Binding AddTrackListCmd}"/>
        <Button Grid.Row="2" Grid.Column="3" Height="22" Margin="2" 
                VerticalAlignment="Top"
                Content="Remove"
                Command="{Binding RemoveTrackListCmd}"/>
    </Grid>
</GroupBox >
Then, add the following to your view model:
. . .
public ICommand AddTrackListCmd { get { return new DelegateCommand(AddTrackList, CanAddTrackList); } }
public ICommand RemoveTrackListCmd { get { return new DelegateCommand(RemoveTrackList, CanRemoveTrackList); } }
. . .
private void AddTrackList(object obj)
{
    _trackServiceEngine.AddTrackList(NewTrackList) ;
    ActiveFilter = false;
    SelectedTrackList = NewTrackList;
    RefreshTrackListDisplay();
}
private bool CanAddTrackList(object obj)
{
    return !string.IsNullOrWhiteSpace(NewTrackList) && TrackLists != null && !TrackLists.Contains(NewTrackList);
}
private void RemoveTrackList(object obj)
{
    _trackServiceEngine.RemoveTrackList(SelectedTrackList);
    RefreshTrackListDisplay();
}
private bool CanRemoveTrackList(object obj)
{
    return SelectedTrackList != null;
}
. . .
public string NewTrackList
{
    get { return _newTrackList; }
    set
    {
        _newTrackList = value;
        NotifyPropertyChanged(() => NewTrackList);
    }
}
public string TrackListFilter
{
    get { return _trackListFilter; }
    set
    {
        _trackListFilter = value;
        NotifyPropertyChanged(() => TrackListFilter);
    }
}
public bool ActiveFilter
{
    get { return _activeFilter; }
    set
    {
        _activeFilter = value;
        RefreshTrackListDisplay();
    }
}
public string SelectedTrackList
{
    get { return _selectedTrackList; }
    set
    {
        _selectedTrackList = value;
        NotifyPropertyChanged(() => SelectedTrackList);
        RefreshTrackDisplay();
    }
}
public List<string> TrackLists { get { return _trackServiceEngine.GetTrackLists(ActiveFilter, TrackListFilter); } }
. . .
internal void RefreshTrackListDisplay()
{
    NotifyPropertyChanged(() => NewTrackList);
    NotifyPropertyChanged(() => ActiveFilter);
    NotifyPropertyChanged(() => TrackListFilter);
    NotifyPropertyChanged(() => SelectedTrackList);
    NotifyPropertyChanged(() => TrackLists);
}
. . .
To handle the text changed events for the text boxes, add the following event handlers to the windows "Code behind"
TrackEditorViewModel _vm;
public MainWindow()
{
    InitializeComponent();
    
    _vm =new TrackEditorViewModel();
    DataContext = _vm;
}
private void NewTrackListChanged(object sender, TextChangedEventArgs e)
{
    var textbox = sender as TextBox;
    if (textbox != null)
    {
        _vm.NewTrackList = textbox.Text;
        _vm.RefreshTrackListDisplay();
    }
}
private void FilterChanged(object sender, TextChangedEventArgs e)
{
    var textbox = sender as TextBox;
    if (textbox != null)
    {
        _vm.TrackListFilter = textbox.Text;
        _vm.RefreshTrackListDisplay();
    }
}
To prevent old information in the view after disconnect, add refresh of the track list display when disconnecting.
. . .
private void Disconnect(object obj)
{
    _trackServiceEngine.Disconnect();
    ConnectionStatus = "Disconnected!";
    RefreshTrackListDisplay();
}
. . .
Track info management
Track display
- Display existing tracks in the selected list
- Filtered view
 
- Add new track
- Select track
- Remove selected track
Add the following to your window xaml:
. . .
<GroupBox Header="Tracks" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Margin="2"
               Content="New track"/>
        <TextBox Grid.Row="0" Grid.Column="1" Margin="2" Height="22"
                 Text="{Binding NewTrack}"
                 TextChanged="NewTrackChanged"/>
        <CheckBox Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" 
                  Content="Id filter" 
                  IsChecked="{Binding ActiveTrackIdFilter}"/>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="2" Height="22"
                 VerticalAlignment="Center"
                 Text="{Binding TrackIdFilter}"                          
                 TextChanged="TrackIdFilterChanged" />
        <StackPanel Grid.Row="3" Grid.Column="0" >
            <Label Margin="2"
                   Content="Tracks"/>
            <StackPanel Orientation="Horizontal" Margin="2">
                <Label Content="#"/>
                <Label Content="{Binding Tracks.Count}"/>
            </StackPanel>
        </StackPanel> 
        
        <ListBox Grid.Row="3" Grid.Column="1" MinHeight="22"  Margin="2"
                 SelectedItem="{Binding SelectedTrack}"
                 ItemsSource="{Binding Tracks}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Width="Auto" Height="Auto" Margin="0"
                               Text="{Binding Path=TrackItemId.InstanceId}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Grid.Row="0" Grid.Column="3" Height="22" Margin="2" 
                Content="Add"
                Command="{Binding AddTrackCmd}"/>
        <Button Grid.Row="1" Grid.Column="3" Height="22" Margin="2" 
                Content="Remove"
                Command="{Binding RemoveTrackCmd}"/>
    </Grid>
</GroupBox>
. . .
Then, add the following to your view model:
. . .
public ICommand AddTrackCmd { get { return new DelegateCommand(AddTrack, CanAddTrack); } }
public ICommand RemoveTrackCmd { get { return new DelegateCommand(RemoveTrack, CanRemoveTrack); } }
. . .
public string NewTrack { get; set; }
public ITrackData SelectedTrack
{
    get { return _selectedTrack; }
    set
    {
        if (_selectedTrack != null && _isDirty)
        { 
            var trackResult = _trackServiceEngine.GetTrackData(SelectedTrackList, _selectedTrack.TrackItemId.InstanceId);
            _selectedTrack = trackResult.Length > 0 ? trackResult[0] : null;
        }
        _selectedTrack = value;
        SelectedField = null;
        Fields = new ObservableCollection<FieldDefItem>();
        if (SelectedTrack != null)
        {
            foreach (var pair in SelectedTrack.Fields)
            {
                Fields.Add(new FieldDefItem (pair ));
            }
        }
        NotifyPropertyChanged(() => SelectedTrack);
    }
}
public ITrackData[] Tracks { get; private set; }
public bool ActiveTrackIdFilter
{
    get { return _activeTrackIdFilter; }
    set
    {
        _activeTrackIdFilter = value;
        RefreshTrackDisplay();
    }
}
public bool ActiveTagFilter
{
    get { return _activeTagFilter; }
    set
    {
        _activeTagFilter = value;
        RefreshTrackDisplay();
    }
}
public string TrackIdFilter { get; set; }
. . .
private void AddTrack(object obj)
{
    UpdateTrack(NewTrack, _defPos, 90.0,  5.0, DateTime.UtcNow, true);
    RefreshTrackDisplay();
}
private bool CanAddTrack(object obj)
{
    return SelectedTrackList != null && !string.IsNullOrWhiteSpace(NewTrack) && !ExistingTrack();
}
private bool ExistingTrack()
{
    foreach (var track in Tracks)
    {
        if (NewTrack == track.TrackItemId.InstanceId)
            return true;
    }
    return false;
}
private void RemoveTrack(object obj)
{
    _trackServiceEngine.RemoveTrack(SelectedTrackList, SelectedTrack.TrackItemId.InstanceId);
    RefreshTrackDisplay();
}
private bool CanRemoveTrack(object obj)
{
    return SelectedTrackList != null && SelectedTrack != null;
}
. . .
internal void RefreshTrackDisplay()
{
    Tracks = _trackServiceEngine.GetAllTracks(SelectedTrackList, ActiveTrackIdFilter, TrackIdFilter, ActiveTagFilter, TagFilterKey, TagFilterValue);
    NotifyPropertyChanged(() => NewTrack);
    NotifyPropertyChanged(() => SelectedTrack);
    NotifyPropertyChanged(() => Tracks);
    NotifyPropertyChanged(() => ActiveTrackIdFilter);
    NotifyPropertyChanged(() => TrackIdFilter);
}
. . .
Handle the text changed events for the text boxes in the "Code behind":
. . .
private void NewTrackChanged(object sender, TextChangedEventArgs e)
{
    var textbox = sender as TextBox;
    if (textbox != null)
    {
        _vm.NewTrack = textbox.Text;
        _vm.RefreshTrackDisplay();
    }
}
private void TrackIdFilterChanged(object sender, TextChangedEventArgs e)
{
    var textbox = sender as TextBox;
    if (textbox != null)
    {
        _vm.TrackIdFilter = textbox.Text;
        _vm.RefreshTrackDisplay();
    }
}
. . .
Edit track information
Add the following to your window xaml:
<GroupBox Grid.Row="6" Header="Edit track" IsEnabled="{Binding EditableTrack}" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Margin="2"
               Content="Track id" />
        <TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Margin="2" 
                   Text="{Binding CurrentId}"/>
        <Button Grid.Row="0" Grid.Column="3" Height="22" Margin="2,4" 
                Content="Save"
                Command="{Binding SaveTrackCmd}"/>
        <Label Grid.Row="1" Grid.Column="0" Margin="2"
               Content="Position" />
        <TextBox Grid.Row="1" Grid.Column="1" Margin="2"                         
                 Text="{Binding CurrentPos}"
                 TextChanged="PositionChanged"/>
        <Label Grid.Row="2" Grid.Column="0" Margin="2"
                Content="Course" />
        <TextBox Grid.Row="2" Grid.Column="1"  Margin="2"                         
                 Text="{Binding CurrentCourse}" TextAlignment="Right"
                 TextChanged="CourseChanged"/>
        <Label Grid.Row="3" Grid.Column="0" Margin="2"
                Content="Speed" />
        <TextBox Grid.Row="3" Grid.Column="1" Margin="2" 
                 Text="{Binding CurrentSpeed}" TextAlignment="Right"  
                 TextChanged="SpeedChanged"/>
        <Label Grid.Row="4" Grid.Column="0" Margin="2"
               Content="Time" />
        <xctk:DateTimePicker Grid.Row="4" Grid.Column="1" Margin="2"
                             Format="Custom" FormatString="yyyy.MM.dd HH:mm:ss" 
                             ShowButtonSpinner="False"                                     
                             Value="{Binding TimeStamp}"
                             IsEnabled="{Binding TimeEnabled}" TimeFormatString="HH:mm:ss" />
        <CheckBox Grid.Row="4" Grid.Column="2" VerticalAlignment="Center"
                  Content="Use current time"
                  IsChecked="{Binding UseCurrentTime}" Margin="0,8"/>
        <Label Grid.Row="5" Grid.Column="0" 
               Content="Available tags" />
        <ListView Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Margin="2"
                  HorizontalContentAlignment="Stretch"
                  SelectedItem="{Binding SelectedField}"
                  ItemsSource="{Binding Fields}"
                  SizeChanged="EqualSpaceListViewSizeChanged">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Tag" Width="auto" DisplayMemberBinding="{Binding Key}" />
                    <GridViewColumn Header="Value" Width="auto" DisplayMemberBinding="{Binding Value}" />
                </GridView>
            </ListView.View>
        </ListView>               
        <Label Grid.Row="6" Grid.Column="0" Margin="2"
               Content="Tag and value" />
        <TextBox Grid.Row="6" Grid.Column="1"  Margin="2"                         
                 Text="{Binding SelectedFieldKey}" 
                 TextChanged="SelectedFieldKeyChanged"/>
        <TextBox Grid.Row="6" Grid.Column="2" Margin="2" 
                 Text="{Binding SelectedFieldValue}"/>
        <Button Grid.Row="6" Grid.Column="3" Height="22" Margin="2,4" 
                Content="Add or update"
                Command="{Binding UpdateFieldsCmd}"/>
    </Grid>
</GroupBox>
And the following to your view model:
. . .
public ICommand UpdateFieldsCmd { get { return new DelegateCommand(AddOrUpdateField, CanAddOrUpdateField); } }
public ICommand SaveTrackCmd { get { return new DelegateCommand(SaveTrack, CanSaveTrack); } }
. . .
public bool ValidTrack { get { return SelectedTrack != null ; } }
public string CurrentId { get { return SelectedTrack?.TrackItemId.InstanceId; } }
public string CurrentPos
{
    get { return SelectedTrack != null && SelectedTrack.Pos.HasValue ? SelectedTrack.Pos.Value.ToString(PositionFormat.GeoDMS) : "-"; }
    set
    {
        if (SelectedTrack == null)
            return;
        GeoPos pos;
        if (GeoPos.TryParse(value, PositionFormat.GeoDMS, out pos))
        {
            SelectedTrack.Pos = pos;
            _isDirty = true;
        }
        NotifyPropertyChanged(() => CurrentPos);
    }
}
public string CurrentCourse
{
    get { return SelectedTrack != null && SelectedTrack.Course.HasValue ? SelectedTrack.Course.Value.ToString("N1") : "-"; }
    set
    {
        if (SelectedTrack == null)
            return;
        double course;
        if (double.TryParse(value, out course ))
        {
            SelectedTrack.Course = course;
            _isDirty = true;
        }
        NotifyPropertyChanged(() => CurrentCourse);
    }
}
public string CurrentSpeed
{
    get { return SelectedTrack != null && SelectedTrack.Speed.HasValue ? SelectedTrack.Speed.Value.ToString("N1") : "-"; }
    set
    {
        if (SelectedTrack == null)
            return;
        double speed;
        if (double.TryParse(value, out speed))
        {
            SelectedTrack.Speed = speed;
            _isDirty = true;
        }
        NotifyPropertyChanged(() => CurrentSpeed);
    }
}
public DateTime TimeStamp
{
    get { return SelectedTrack != null && SelectedTrack.ObservationTime.HasValue ? SelectedTrack.ObservationTime.Value : DateTime.MinValue; }
    set
    {
        if (SelectedTrack != null)
        {
            SelectedTrack.ObservationTime = value;
            _isDirty = true;
        }
        
        NotifyPropertyChanged(() => TimeStamp);
    }
}
public bool TimeEnabled { get { return SelectedTrack != null && !UseCurrentTime; } }
public bool UseCurrentTime
{
    get { return _useCurrentTime; }
    set
    {
        _useCurrentTime = value;
        _isDirty = true;
        RefreshTrackEditDisplay();
    }
}
public string SelectedFieldKey
{
    get { return !string.IsNullOrWhiteSpace(_selectedFieldKey) ? _selectedFieldKey : ""; }
    set
    {
        _selectedFieldKey = value;
        NotifyPropertyChanged(() => SelectedFieldKey);
    }
}
public string SelectedFieldValue
{
    get { return !string.IsNullOrWhiteSpace(_selectedFieldValue) ? _selectedFieldValue : ""; }
    set
    {
        _selectedFieldValue = value;
        NotifyPropertyChanged(() => SelectedFieldValue);
    }
}
public FieldDefItem SelectedField
{
    get { return _selectedField; }
    set
    {
        _selectedField = value;
        SelectedFieldKey = _selectedField?.Key;
        SelectedFieldValue = _selectedField?.Value;
        NotifyPropertyChanged(() => SelectedFieldKey);
    }
}
public ObservableCollection<FieldDefItem> Fields { get; private set; } 
. . .
private void AddOrUpdateField(object obj)
{
    var found = false;
    foreach (var field in Fields)
    {
        if (field.Key == SelectedFieldKey)
        {
            field.Value = SelectedFieldValue;
            found = true;
        }
    }
    if (!found)
        Fields.Add(new FieldDefItem(SelectedFieldKey, SelectedFieldValue));
    SelectedTrack.Fields[SelectedFieldKey] = SelectedFieldValue;
    _isDirty = true;
    RefreshTrackEditDisplay();
}
private bool CanAddOrUpdateField(object obj)
{
    return SelectedTrackList != null && SelectedTrack != null &&  !string.IsNullOrWhiteSpace(_selectedFieldKey);
}
private void SaveTrack(object obj)
{
    if (UseCurrentTime)
        SelectedTrack.ObservationTime = DateTime.UtcNow;
    _trackServiceEngine.AddOrUpdateTrack(SelectedTrackList, SelectedTrack);
    _isDirty = false;
    RefreshTrackEditDisplay();
}
private bool CanSaveTrack(object obj)
{
    return _isDirty || UseCurrentTime;
}
. . .
private void RefreshTrackEditDisplay()
{
    NotifyPropertyChanged(() => SelectedTrack);
    NotifyPropertyChanged(() => ValidTrack);
    NotifyPropertyChanged(() => EditableTrack);
    NotifyPropertyChanged(() => UseCurrentTime);
    NotifyPropertyChanged(() => TimeEnabled);
    NotifyPropertyChanged(() => CurrentId);
    NotifyPropertyChanged(() => CurrentPos);
    NotifyPropertyChanged(() => CurrentSpeed);
    NotifyPropertyChanged(() => CurrentCourse);
    NotifyPropertyChanged(() => TimeStamp);
    NotifyPropertyChanged(() => Fields);
    NotifyPropertyChanged(() => SelectedField);
    NotifyPropertyChanged(() => SelectedFieldKey);
    NotifyPropertyChanged(() => SelectedFieldValue);
}
. . .
And again, handle the text changed events for the text boxes in the "Code behind":
. . .
. . .
Track filtering by tag fields
. . .
Auto refresh track info
. . .





