Added filtering and sorting to shipcalls in the list.

While doing so, I have also refactored the shipcall processing logic in the main window.
All changes now go through the filter and sorting stage before all controls are removed
and only the controls matching to the sorted list are added to the stack panel.
This commit is contained in:
Daniel Schick 2023-09-12 16:48:28 +02:00
parent 90338f9e95
commit 7660ee72f2
16 changed files with 633 additions and 245 deletions

View File

@ -1,8 +1,8 @@
//----------------------
// <auto-generated>
// Generated REST API Client Code Generator v1.7.17.0 on 05.09.2023 16:40:43
// Using the tool OpenAPI Generator v6.6.0
// Generated REST API Client Code Generator v1.8.4.0 on 11.09.2023 10:29:36
// Using the tool OpenAPI Generator v7.0.0
// </auto-generated>
//----------------------
@ -29,7 +29,8 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Net.Security;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
@ -2334,7 +2335,6 @@ namespace BreCalClient.misc.Client
internal class CustomJsonCodec : IRestSerializer, ISerializer, IDeserializer
{
private readonly IReadableConfiguration _configuration;
private static readonly string _contentType = "application/json";
private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings
{
// OpenAPI generated types generally hide default constructors.
@ -2435,15 +2435,11 @@ namespace BreCalClient.misc.Client
}
public ISerializer Serializer => this;
public IDeserializer Deserializer => this;
public string[] AcceptedContentTypes => RestSharp.Serializers.ContentType.JsonAccept;
public string[] AcceptedContentTypes => RestSharp.ContentType.JsonAccept;
public SupportsContentType SupportsContentType => contentType =>
contentType.EndsWith("json", StringComparison.InvariantCultureIgnoreCase) ||
contentType.EndsWith("javascript", StringComparison.InvariantCultureIgnoreCase);
public string ContentType
{
get { return _contentType; }
set { throw new InvalidOperationException("Not allowed to set content type."); }
}
contentType.Value.EndsWith("json", StringComparison.InvariantCultureIgnoreCase) ||
contentType.Value.EndsWith("javascript", StringComparison.InvariantCultureIgnoreCase);
public ContentType ContentType { get; set; } = RestSharp.ContentType.Json;
public DataFormat DataFormat => DataFormat.Json;
}
/// <summary>
@ -2688,7 +2684,7 @@ namespace BreCalClient.misc.Client
}
return transformed;
}
private ApiResponse<T> Exec<T>(RestRequest req, RequestOptions options, IReadableConfiguration configuration)
private ApiResponse<T> Exec<T>(RestRequest request, RequestOptions options, IReadableConfiguration configuration)
{
var baseUrl = configuration.GetOperationServerUrl(options.Operation, options.OperationIndex) ?? _baseUrl;
var cookies = new CookieContainer();
@ -2705,84 +2701,87 @@ namespace BreCalClient.misc.Client
CookieContainer = cookies,
MaxTimeout = configuration.Timeout,
Proxy = configuration.Proxy,
UserAgent = configuration.UserAgent
UserAgent = configuration.UserAgent,
UseDefaultCredentials = configuration.UseDefaultCredentials,
RemoteCertificateValidationCallback = configuration.RemoteCertificateValidationCallback
};
RestClient client = new RestClient(clientOptions)
.UseSerializer(() => new CustomJsonCodec(SerializerSettings, configuration));
InterceptRequest(req);
RestResponse<T> response;
if (RetryConfiguration.RetryPolicy != null)
using (RestClient client = new RestClient(clientOptions,
configureSerialization: serializerConfig => serializerConfig.UseSerializer(() => new CustomJsonCodec(SerializerSettings, configuration))))
{
var policy = RetryConfiguration.RetryPolicy;
var policyResult = policy.ExecuteAndCapture(() => client.Execute(req));
response = (policyResult.Outcome == OutcomeType.Successful) ? client.Deserialize<T>(policyResult.Result) : new RestResponse<T>
InterceptRequest(request);
RestResponse<T> response;
if (RetryConfiguration.RetryPolicy != null)
{
Request = req,
ErrorException = policyResult.FinalException
};
}
else
{
response = client.Execute<T>(req);
}
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
if (typeof(BreCalClient.misc.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
{
try
{
response.Data = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
}
catch (Exception ex)
{
throw ex.InnerException != null ? ex.InnerException : ex;
}
}
else if (typeof(T).Name == "Stream") // for binary response
{
response.Data = (T)(object)new MemoryStream(response.RawBytes);
}
else if (typeof(T).Name == "Byte[]") // for byte response
{
response.Data = (T)(object)response.RawBytes;
}
else if (typeof(T).Name == "String") // for string response
{
response.Data = (T)(object)response.Content;
}
InterceptResponse(req, response);
var result = ToApiResponse(response);
if (response.ErrorMessage != null)
{
result.ErrorText = response.ErrorMessage;
}
if (response.Cookies != null && response.Cookies.Count > 0)
{
if (result.Cookies == null) result.Cookies = new List<Cookie>();
foreach (var restResponseCookie in response.Cookies.Cast<Cookie>())
{
var cookie = new Cookie(
restResponseCookie.Name,
restResponseCookie.Value,
restResponseCookie.Path,
restResponseCookie.Domain
)
var policy = RetryConfiguration.RetryPolicy;
var policyResult = policy.ExecuteAndCapture(() => client.Execute(request));
response = (policyResult.Outcome == OutcomeType.Successful) ? client.Deserialize<T>(policyResult.Result) : new RestResponse<T>(request)
{
Comment = restResponseCookie.Comment,
CommentUri = restResponseCookie.CommentUri,
Discard = restResponseCookie.Discard,
Expired = restResponseCookie.Expired,
Expires = restResponseCookie.Expires,
HttpOnly = restResponseCookie.HttpOnly,
Port = restResponseCookie.Port,
Secure = restResponseCookie.Secure,
Version = restResponseCookie.Version
ErrorException = policyResult.FinalException
};
result.Cookies.Add(cookie);
}
else
{
response = client.Execute<T>(request);
}
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
if (typeof(BreCalClient.misc.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
{
try
{
response.Data = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
}
catch (Exception ex)
{
throw ex.InnerException != null ? ex.InnerException : ex;
}
}
else if (typeof(T).Name == "Stream") // for binary response
{
response.Data = (T)(object)new MemoryStream(response.RawBytes);
}
else if (typeof(T).Name == "Byte[]") // for byte response
{
response.Data = (T)(object)response.RawBytes;
}
else if (typeof(T).Name == "String") // for string response
{
response.Data = (T)(object)response.Content;
}
InterceptResponse(request, response);
var result = ToApiResponse(response);
if (response.ErrorMessage != null)
{
result.ErrorText = response.ErrorMessage;
}
if (response.Cookies != null && response.Cookies.Count > 0)
{
if (result.Cookies == null) result.Cookies = new List<Cookie>();
foreach (var restResponseCookie in response.Cookies.Cast<Cookie>())
{
var cookie = new Cookie(
restResponseCookie.Name,
restResponseCookie.Value,
restResponseCookie.Path,
restResponseCookie.Domain
)
{
Comment = restResponseCookie.Comment,
CommentUri = restResponseCookie.CommentUri,
Discard = restResponseCookie.Discard,
Expired = restResponseCookie.Expired,
Expires = restResponseCookie.Expires,
HttpOnly = restResponseCookie.HttpOnly,
Port = restResponseCookie.Port,
Secure = restResponseCookie.Secure,
Version = restResponseCookie.Version
};
result.Cookies.Add(cookie);
}
}
return result;
}
return result;
}
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest req, RequestOptions options, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
private async Task<ApiResponse<T>> ExecAsync<T>(RestRequest request, RequestOptions options, IReadableConfiguration configuration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var baseUrl = configuration.GetOperationServerUrl(options.Operation, options.OperationIndex) ?? _baseUrl;
var clientOptions = new RestClientOptions(baseUrl)
@ -2790,71 +2789,73 @@ namespace BreCalClient.misc.Client
ClientCertificates = configuration.ClientCertificates,
MaxTimeout = configuration.Timeout,
Proxy = configuration.Proxy,
UserAgent = configuration.UserAgent
UserAgent = configuration.UserAgent,
UseDefaultCredentials = configuration.UseDefaultCredentials
};
RestClient client = new RestClient(clientOptions)
.UseSerializer(() => new CustomJsonCodec(SerializerSettings, configuration));
InterceptRequest(req);
RestResponse<T> response;
if (RetryConfiguration.AsyncRetryPolicy != null)
using (RestClient client = new RestClient(clientOptions,
configureSerialization: serializerConfig => serializerConfig.UseSerializer(() => new CustomJsonCodec(SerializerSettings, configuration))))
{
var policy = RetryConfiguration.AsyncRetryPolicy;
var policyResult = await policy.ExecuteAndCaptureAsync((ct) => client.ExecuteAsync(req, ct), cancellationToken).ConfigureAwait(false);
response = (policyResult.Outcome == OutcomeType.Successful) ? client.Deserialize<T>(policyResult.Result) : new RestResponse<T>
InterceptRequest(request);
RestResponse<T> response;
if (RetryConfiguration.AsyncRetryPolicy != null)
{
Request = req,
ErrorException = policyResult.FinalException
};
}
else
{
response = await client.ExecuteAsync<T>(req, cancellationToken).ConfigureAwait(false);
}
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
if (typeof(BreCalClient.misc.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
{
response.Data = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
}
else if (typeof(T).Name == "Stream") // for binary response
{
response.Data = (T)(object)new MemoryStream(response.RawBytes);
}
else if (typeof(T).Name == "Byte[]") // for byte response
{
response.Data = (T)(object)response.RawBytes;
}
InterceptResponse(req, response);
var result = ToApiResponse(response);
if (response.ErrorMessage != null)
{
result.ErrorText = response.ErrorMessage;
}
if (response.Cookies != null && response.Cookies.Count > 0)
{
if (result.Cookies == null) result.Cookies = new List<Cookie>();
foreach (var restResponseCookie in response.Cookies.Cast<Cookie>())
{
var cookie = new Cookie(
restResponseCookie.Name,
restResponseCookie.Value,
restResponseCookie.Path,
restResponseCookie.Domain
)
var policy = RetryConfiguration.AsyncRetryPolicy;
var policyResult = await policy.ExecuteAndCaptureAsync((ct) => client.ExecuteAsync(request, ct), cancellationToken).ConfigureAwait(false);
response = (policyResult.Outcome == OutcomeType.Successful) ? client.Deserialize<T>(policyResult.Result) : new RestResponse<T>(request)
{
Comment = restResponseCookie.Comment,
CommentUri = restResponseCookie.CommentUri,
Discard = restResponseCookie.Discard,
Expired = restResponseCookie.Expired,
Expires = restResponseCookie.Expires,
HttpOnly = restResponseCookie.HttpOnly,
Port = restResponseCookie.Port,
Secure = restResponseCookie.Secure,
Version = restResponseCookie.Version
ErrorException = policyResult.FinalException
};
result.Cookies.Add(cookie);
}
else
{
response = await client.ExecuteAsync<T>(request, cancellationToken).ConfigureAwait(false);
}
// if the response type is oneOf/anyOf, call FromJSON to deserialize the data
if (typeof(BreCalClient.misc.Model.AbstractOpenAPISchema).IsAssignableFrom(typeof(T)))
{
response.Data = (T) typeof(T).GetMethod("FromJson").Invoke(null, new object[] { response.Content });
}
else if (typeof(T).Name == "Stream") // for binary response
{
response.Data = (T)(object)new MemoryStream(response.RawBytes);
}
else if (typeof(T).Name == "Byte[]") // for byte response
{
response.Data = (T)(object)response.RawBytes;
}
InterceptResponse(request, response);
var result = ToApiResponse(response);
if (response.ErrorMessage != null)
{
result.ErrorText = response.ErrorMessage;
}
if (response.Cookies != null && response.Cookies.Count > 0)
{
if (result.Cookies == null) result.Cookies = new List<Cookie>();
foreach (var restResponseCookie in response.Cookies.Cast<Cookie>())
{
var cookie = new Cookie(
restResponseCookie.Name,
restResponseCookie.Value,
restResponseCookie.Path,
restResponseCookie.Domain
)
{
Comment = restResponseCookie.Comment,
CommentUri = restResponseCookie.CommentUri,
Discard = restResponseCookie.Discard,
Expired = restResponseCookie.Expired,
Expires = restResponseCookie.Expires,
HttpOnly = restResponseCookie.HttpOnly,
Port = restResponseCookie.Port,
Secure = restResponseCookie.Secure,
Version = restResponseCookie.Version
};
result.Cookies.Add(cookie);
}
}
return result;
}
return result;
}
#region IAsynchronousClient
/// <summary>
@ -3521,6 +3522,7 @@ namespace BreCalClient.misc.Client
/// Example: http://localhost:3000/v1/
/// </summary>
private string _basePath;
private bool _useDefaultCredentials = false;
/// <summary>
/// Gets or sets the API key based on the authentication name.
/// This is the key and value comprising the "secret" for accessing an API.
@ -3610,11 +3612,20 @@ namespace BreCalClient.misc.Client
/// <summary>
/// Gets or sets the base path for API access.
/// </summary>
public virtual string BasePath {
public virtual string BasePath
{
get { return _basePath; }
set { _basePath = value; }
}
/// <summary>
/// Determine whether or not the "default credentials" (e.g. the user account under which the current process is running) will be sent along to the server. The default is false.
/// </summary>
public virtual bool UseDefaultCredentials
{
get { return _useDefaultCredentials; }
set { _useDefaultCredentials = value; }
}
/// <summary>
/// Gets or sets the default header.
/// </summary>
[Obsolete("Use DefaultHeaders instead.")]
@ -3854,7 +3865,7 @@ namespace BreCalClient.misc.Client
/// <return>The operation server URL.</return>
public string GetOperationServerUrl(string operation, int index, Dictionary<string, string> inputVariables)
{
if (OperationServers.TryGetValue(operation, out var operationServer))
if (operation != null && OperationServers.TryGetValue(operation, out var operationServer))
{
return GetServerUrl(operationServer, index, inputVariables);
}
@ -3905,6 +3916,10 @@ namespace BreCalClient.misc.Client
}
return url;
}
/// <summary>
/// Gets and Sets the RemoteCertificateValidationCallback
/// </summary>
public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
#endregion Properties
#region Methods
/// <summary>
@ -3970,6 +3985,8 @@ namespace BreCalClient.misc.Client
TempFolderPath = second.TempFolderPath ?? first.TempFolderPath,
DateTimeFormat = second.DateTimeFormat ?? first.DateTimeFormat,
ClientCertificates = second.ClientCertificates ?? first.ClientCertificates,
UseDefaultCredentials = second.UseDefaultCredentials,
RemoteCertificateValidationCallback = second.RemoteCertificateValidationCallback ?? first.RemoteCertificateValidationCallback,
};
return config;
}
@ -4294,6 +4311,10 @@ namespace BreCalClient.misc.Client
/// <value>Password.</value>
string Password { get; }
/// <summary>
/// Determine whether or not the "default credentials" (e.g. the user account under which the current process is running) will be sent along to the server. The default is false.
/// </summary>
bool UseDefaultCredentials { get; }
/// <summary>
/// Get the servers associated with the operation.
/// </summary>
/// <value>Operation servers.</value>
@ -4316,6 +4337,11 @@ namespace BreCalClient.misc.Client
/// </summary>
/// <value>X509 Certificate collection.</value>
X509CertificateCollection ClientCertificates { get; }
/// <summary>
/// Callback function for handling the validation of remote certificates. Useful for certificate pinning and
/// overriding certificate errors in the scope of a request.
/// </summary>
RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; }
}
}
@ -4877,16 +4903,16 @@ namespace BreCalClient.misc.Model
/// <param name="id">id.</param>
/// <param name="name">name.</param>
/// <param name="participantId">participantId.</param>
/// <param name="_lock">_lock.</param>
/// <param name="varLock">varLock.</param>
/// <param name="created">created.</param>
/// <param name="modified">modified.</param>
/// <param name="deleted">deleted (default to false).</param>
public Berth(int id = default(int), string name = default(string), int? participantId = default(int?), bool? _lock = default(bool?), DateTime created = default(DateTime), DateTime? modified = default(DateTime?), bool deleted = false)
public Berth(int id = default(int), string name = default(string), int? participantId = default(int?), bool? varLock = default(bool?), DateTime created = default(DateTime), DateTime? modified = default(DateTime?), bool deleted = false)
{
this.Id = id;
this.Name = name;
this.ParticipantId = participantId;
this.Lock = _lock;
this.VarLock = varLock;
this.Created = created;
this.Modified = modified;
this.Deleted = deleted;
@ -4907,10 +4933,10 @@ namespace BreCalClient.misc.Model
[DataMember(Name = "participant_id", EmitDefaultValue = true)]
public int? ParticipantId { get; set; }
/// <summary>
/// Gets or Sets Lock
/// Gets or Sets VarLock
/// </summary>
[DataMember(Name = "lock", EmitDefaultValue = true)]
public bool? Lock { get; set; }
public bool? VarLock { get; set; }
/// <summary>
/// Gets or Sets Created
/// </summary>
@ -4937,7 +4963,7 @@ namespace BreCalClient.misc.Model
sb.Append(" Id: ").Append(Id).Append("\n");
sb.Append(" Name: ").Append(Name).Append("\n");
sb.Append(" ParticipantId: ").Append(ParticipantId).Append("\n");
sb.Append(" Lock: ").Append(Lock).Append("\n");
sb.Append(" VarLock: ").Append(VarLock).Append("\n");
sb.Append(" Created: ").Append(Created).Append("\n");
sb.Append(" Modified: ").Append(Modified).Append("\n");
sb.Append(" Deleted: ").Append(Deleted).Append("\n");
@ -4988,9 +5014,9 @@ namespace BreCalClient.misc.Model
this.ParticipantId.Equals(input.ParticipantId))
) &&
(
this.Lock == input.Lock ||
(this.Lock != null &&
this.Lock.Equals(input.Lock))
this.VarLock == input.VarLock ||
(this.VarLock != null &&
this.VarLock.Equals(input.VarLock))
) &&
(
this.Created == input.Created ||
@ -5025,9 +5051,9 @@ namespace BreCalClient.misc.Model
{
hashCode = (hashCode * 59) + this.ParticipantId.GetHashCode();
}
if (this.Lock != null)
if (this.VarLock != null)
{
hashCode = (hashCode * 59) + this.Lock.GetHashCode();
hashCode = (hashCode * 59) + this.VarLock.GetHashCode();
}
if (this.Created != null)
{
@ -6521,6 +6547,7 @@ namespace BreCalClient.misc.Model
/// <summary>
/// Gets or Sets Participants
/// </summary>
/// <example>[1,5,7]</example>
[DataMember(Name = "participants", EmitDefaultValue = true)]
public List<int> Participants { get; set; }
/// <summary>

View File

@ -16,6 +16,9 @@
<setting name="APP_TITLE" serializeAs="String">
<value>!!Bremen calling Testversion!!</value>
</setting>
<setting name="LOGO_IMAGE_URL" serializeAs="String">
<value>https://www.textbausteine.net/</value>
</setting>
</BreCalClient.Properties.Settings>
</applicationSettings>
</configuration>

View File

@ -95,12 +95,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" />
<PackageReference Include="JsonSubTypes" Version="2.0.1" />
<PackageReference Include="log4net" Version="2.0.15" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="7.2.3" />
<PackageReference Include="RestSharp" Version="108.0.2" />
<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="RestSharp" Version="110.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -61,6 +61,13 @@ namespace BreCalClient
Shifting = 3
}
public enum SortOrder
{
SHIP_NAME,
ETA_ETD,
MODIFIED
}
#endregion
#region public helper

View File

@ -58,11 +58,17 @@
<ColumnDefinition Width="60" />
<ColumnDefinition Width=".1*" />
<ColumnDefinition Width=".2*" />
<ColumnDefinition Width=".6*" />
<ColumnDefinition Width=".1*" />
<ColumnDefinition Width=".2*" />
<ColumnDefinition Width=".1*" />
<ColumnDefinition Width=".2*" />
</Grid.ColumnDefinitions>
<Button Margin="2" Grid.Column="0" Content="{x:Static p:Resources.textNewDots}" x:Name="buttonNew" Visibility="Hidden" Click="buttonNew_Click" Background="Transparent"/>
<Label Content="{x:Static p:Resources.textSortOrder}" Grid.Column="1" HorizontalContentAlignment="Right"/>
<ComboBox x:Name="comboBoxSortOrder" Margin="2" Grid.Column="2" />
<ComboBox x:Name="comboBoxSortOrder" Margin="2" Grid.Column="2" SelectionChanged="comboBoxSortOrder_SelectionChanged" />
<CheckBox x:Name="checkboxShowCancelledCalls" Grid.Column="3" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="2" Checked="checkboxShowCancelledCalls_Checked" Unchecked="checkboxShowCancelledCalls_Checked" />
<Label Content="{x:Static p:Resources.textShowCancelledShipcalls}" Grid.Column="4" />
<Button Margin="2" Grid.Column="6" Content="{x:Static p:Resources.textClearFilters}" x:Name="buttonClearFilter" Click="buttonClearFilter_Click" Background="Transparent" />
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>

View File

@ -9,10 +9,12 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using static BreCalClient.Extensions;
namespace BreCalClient
{
@ -22,22 +24,35 @@ namespace BreCalClient
public partial class MainWindow : Window
{
private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30;
#region Fields
private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30;
private readonly DefaultApi _api;
private readonly ObservableCollection<ShipcallControlModel> _controlModels = new();
private readonly Dictionary<int, ShipcallControlModel> _allShipcallsDict = new();
private readonly Dictionary<int, ShipcallControl> _allShipCallsControlDict = new();
private readonly List<ShipcallControlModel> _visibleControlModels = new();
private List<Ship> _ships = new();
private readonly ConcurrentDictionary<int, Ship> _shipLookupDict = new();
private List<Berth> _berths = new();
private readonly ConcurrentDictionary<int, Berth> _berthLookupDict = new();
private List<Participant> _participants = new();
private readonly Dictionary<int, Participant> _participantLookupDict = new();
private readonly Dictionary<int, ShipcallControl> _shipCallControlDict = new();
private readonly DefaultApi _api;
private readonly CancellationTokenSource _tokenSource = new();
private LoginResult? _loginResult;
private bool _refreshImmediately = false;
private bool? _showCanceled = null;
private Extensions.SortOrder? _sortOrder;
private bool _filterChanged = false;
private bool _sequenceChanged = false;
#endregion
#region Enums
@ -70,6 +85,12 @@ namespace BreCalClient
labelVersion.Text = "V. " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
if (!string.IsNullOrEmpty(Properties.Settings.Default.APP_TITLE))
this.Title = Properties.Settings.Default.APP_TITLE;
searchFilterControl.SearchFilterChanged += SearchFilterControl_SearchFilterChanged;
searchFilterControl.LogoImageClicked += () =>
{
Process.Start("explorer", Properties.Settings.Default.LOGO_IMAGE_URL);
};
this.comboBoxSortOrder.ItemsSource = Enum.GetValues(typeof(Extensions.SortOrder));
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
@ -134,10 +155,13 @@ namespace BreCalClient
// create UI & save new dialog model
if (esc.ShipcallModel.Shipcall != null)
{
this.UpdateShipcallUI(esc.ShipcallModel.Shipcall, new List<Times>());
this._api.ShipcallsPost(esc.ShipcallModel.Shipcall);
_refreshImmediately = true;
_tokenSource.Cancel();
this.UpdateUI();
this._api.ShipcallsPost(esc.ShipcallModel.Shipcall); // save new ship call
this.AddShipcall(esc.ShipcallModel);
_refreshImmediately = true; // set flag to avoid timer loop termination
_tokenSource.Cancel(); // force timer loop end
}
}
}
@ -175,9 +199,35 @@ namespace BreCalClient
ad.ShowDialog();
}
private void buttonClearFilter_Click(object sender, RoutedEventArgs e)
{
this.searchFilterControl.ClearFilters();
this.checkboxShowCancelledCalls.IsChecked = false;
this.FilterShipcalls();
}
private void SearchFilterControl_SearchFilterChanged()
{
this.FilterShipcalls();
this.UpdateUI();
}
private void checkboxShowCancelledCalls_Checked(object sender, RoutedEventArgs e)
{
this._showCanceled = this.checkboxShowCancelledCalls.IsChecked;
this.SearchFilterControl_SearchFilterChanged();
}
private void comboBoxSortOrder_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
_sortOrder = (Extensions.SortOrder) this.comboBoxSortOrder.SelectedIndex;
this.FilterShipcalls();
this.UpdateUI();
}
#endregion
#region private methods
#region network operations
private async void LoadStaticLists()
{
@ -207,12 +257,6 @@ namespace BreCalClient
_ = Task.Run(() => RefreshShipcalls());
}
private void EnableControlsForParticipant()
{
if (App.Participant.IsTypeFlagSet(Extensions.ParticipantType.BSMD))
this.buttonNew.Visibility = Visibility.Visible;
}
public async Task RefreshShipcalls()
{
while (!_tokenSource.Token.IsCancellationRequested || _refreshImmediately)
@ -236,30 +280,45 @@ namespace BreCalClient
labelStatusBar.Text = ex.Message;
}));
}
if (shipcalls != null)
{
foreach (Shipcall shipcall in shipcalls)
{
// load times for each shipcall
List<Times> currentTimes = await _api.TimesGetAsync(shipcall.Id);
this.UpdateShipcallUI(shipcall, currentTimes);
}
List<ShipcallControl> removeList = new();
foreach (ShipcallControlModel scm in this._controlModels)
{
if (shipcalls.Find(s => s.Id == scm.Shipcall?.Id) == null) // the model is no longer in the search result
if(!_allShipcallsDict.ContainsKey(shipcall.Id))
{
if((scm.Shipcall != null) && this._shipCallControlDict.ContainsKey(scm.Shipcall.Id))
// add entry
ShipcallControlModel scm = new()
{
this.Dispatcher.Invoke((Action)(() =>
{
this.stackPanel.Children.Remove(this._shipCallControlDict[scm.Shipcall.Id]);
}));
this._shipCallControlDict.Remove(scm.Shipcall.Id);
}
Shipcall = shipcall,
Times = currentTimes
};
this.AddShipcall(scm);
}
else
{
// update entry
_allShipcallsDict[shipcall.Id].Shipcall = shipcall;
_allShipcallsDict[shipcall.Id].Times = currentTimes;
this.UpdateShipcall(_allShipcallsDict[shipcall.Id]);
}
}
List<int> existingIds = new(this._allShipcallsDict.Keys);
foreach (int existingId in existingIds)
{
if (shipcalls.Find(s => s.Id == existingId) == null) // the model is no longer in the search result
{
this.RemoveShipcall(existingId);
}
}
this.FilterShipcalls();
this.UpdateUI();
}
try
@ -270,60 +329,167 @@ namespace BreCalClient
}
}
private void UpdateShipcallUI(Shipcall shipcall, List<Times> times)
{
ShipcallControlModel? selectedSCMModel = null;
#endregion
foreach (ShipcallControlModel scm in this._controlModels)
#region basic operations
private void AddShipcall(ShipcallControlModel scm)
{
if (scm.Shipcall == null) return;
_allShipcallsDict[scm.Shipcall.Id] = scm;
Shipcall shipcall = scm.Shipcall;
if (this._shipLookupDict.ContainsKey(shipcall.ShipId))
scm.Ship = this._shipLookupDict[shipcall.ShipId];
if (this._berthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0))
scm.Berth = this._berthLookupDict[shipcall.ArrivalBerthId ?? 0].Name;
scm.AssignParticipants(this._participants);
this.Dispatcher.Invoke(() =>
{
if (scm.Shipcall?.Id == shipcall.Id)
ShipcallControl sc = new()
{
selectedSCMModel = scm;
break;
Height = 120,
ShipcallControlModel = scm,
ParticipantDict = _participantLookupDict
};
sc.EditTimesRequested += Sc_EditTimesRequested;
sc.EditRequested += Sc_EditRequested;
sc.RefreshData();
this._allShipCallsControlDict[scm.Shipcall.Id] = sc;
});
}
private void UpdateShipcall(ShipcallControlModel scm)
{
if(scm.Shipcall == null) return;
Shipcall shipcall = scm.Shipcall;
if (this._shipLookupDict.ContainsKey(shipcall.ShipId))
scm.Ship = this._shipLookupDict[shipcall.ShipId];
if (this._berthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0))
scm.Berth = this._berthLookupDict[shipcall.ArrivalBerthId ?? 0].Name;
scm.AssignParticipants(this._participants);
}
private void RemoveShipcall(int shipcallId)
{
this.Dispatcher.Invoke(() =>
{
this.stackPanel.Children.Remove(this._allShipCallsControlDict[shipcallId]);
});
ShipcallControlModel removeModel = this._allShipcallsDict[shipcallId];
_visibleControlModels.Remove(removeModel);
this._allShipCallsControlDict.Remove(shipcallId);
this._allShipcallsDict.Remove(shipcallId);
}
private void FilterShipcalls()
{
SearchFilterModel sfm = this.searchFilterControl.SearchFilter;
this._visibleControlModels.Clear();
// first add everything
this._visibleControlModels.AddRange(_allShipcallsDict.Values);
// now remove elements whose filter criteria are met
if(sfm.Berths.Count > 0 )
{
this._visibleControlModels.RemoveAll(x => !sfm.Berths.Contains((x.Shipcall?.ArrivalBerthId) ?? -1));
}
if(sfm.Agencies.Count > 0 )
{
this._visibleControlModels.RemoveAll(x => !sfm.Agencies.Contains((x.GetParticipantIdForType(Extensions.ParticipantType.AGENCY)) ?? -1));
}
if(sfm.Categories.Count > 0 )
{
this._visibleControlModels.RemoveAll(x => !sfm.Categories.Contains((x.Shipcall?.Type) ?? -1));
}
if(!string.IsNullOrEmpty(sfm.SearchString))
{
this._visibleControlModels.RemoveAll(x => !x.ContainsRemarkText(sfm.SearchString));
}
if(sfm.ShipLengthTo != null)
{
this._visibleControlModels.RemoveAll(x => x.Ship?.Length > sfm.ShipLengthTo);
}
if(sfm.ShipLengthFrom != null)
{
this._visibleControlModels.RemoveAll(x => x.Ship?.Length < sfm.ShipLengthFrom);
}
if(sfm.EtaFrom != null)
{
this._visibleControlModels.RemoveAll(x => x.Shipcall?.Eta < sfm.EtaFrom);
}
if(sfm.EtaTo != null)
{
this._visibleControlModels.RemoveAll(x => x.Shipcall?.Eta > sfm.EtaTo);
}
if(!_showCanceled ?? true) // canceled calls are filtered by default
{
this._visibleControlModels.RemoveAll(x => x.Shipcall?.Canceled ?? true);
}
if (this._sortOrder != null)
{
switch(this._sortOrder)
{
case Extensions.SortOrder.SHIP_NAME:
this._visibleControlModels.Sort((x, y) => { if (x.Ship == null) return 0; if (y.Ship == null) return 0; return x.Ship.Name.CompareTo(y.Ship.Name); });
break;
case Extensions.SortOrder.MODIFIED:
this._visibleControlModels.Sort((x, y) => { if (x.Shipcall == null) return 0; if (y.Shipcall == null) return 0; return DateTime.Compare(x.Shipcall.Modified ?? x.Shipcall.Created, y.Shipcall.Modified ?? x.Shipcall.Created); });
break;
case Extensions.SortOrder.ETA_ETD:
this._visibleControlModels.Sort((x, y) =>
{
if (x.Shipcall == null) return 0;
if (y.Shipcall == null) return 0;
DateTime xDate = (x.Shipcall.Type == (int) Extensions.TypeEnum.Incoming) ? x.Shipcall.Eta : x.Shipcall.Etd ?? x.Shipcall.Eta;
DateTime yDate = (y.Shipcall.Type == (int) Extensions.TypeEnum.Incoming) ? y.Shipcall.Eta : y.Shipcall.Etd ?? y.Shipcall.Eta;
return DateTime.Compare(xDate, yDate);
});
break;
default:
break;
}
}
}
if (selectedSCMModel != null)
{
selectedSCMModel.Shipcall = shipcall;
selectedSCMModel.Times = times;
}
else
{
// no: create new entry
selectedSCMModel = new()
{
Shipcall = shipcall,
Times = times
};
if (this._shipLookupDict.ContainsKey(shipcall.ShipId))
selectedSCMModel.Ship = this._shipLookupDict[shipcall.ShipId];
if (this._berthLookupDict.ContainsKey(shipcall.ArrivalBerthId ?? 0))
selectedSCMModel.Berth = this._berthLookupDict[shipcall.ArrivalBerthId ?? 0].Name;
_controlModels.Add(selectedSCMModel);
this.Dispatcher.Invoke(new Action(() =>
#endregion
private void UpdateUI()
{
this.Dispatcher.Invoke(new Action(() =>
{
this.stackPanel.Children.Clear();
foreach(ShipcallControlModel visibleModel in this._visibleControlModels)
{
ShipcallControl sc = new()
if (visibleModel.Shipcall == null) continue; // should not happen
if(this._allShipCallsControlDict.ContainsKey(visibleModel.Shipcall.Id))
{
Height = 120,
ShipcallControlModel = selectedSCMModel,
ParticipantDict = _participantLookupDict
};
sc.EditTimesRequested += Sc_EditTimesRequested;
sc.EditRequested += Sc_EditRequested;
this.stackPanel.Children.Add(sc);
this._shipCallControlDict[shipcall.Id] = sc;
}));
}
selectedSCMModel.AssignParticipants(this._participants);
this.Dispatcher.Invoke((Action)(() =>
{
this._shipCallControlDict[shipcall.Id].RefreshData();
this._allShipCallsControlDict[visibleModel.Shipcall.Id].RefreshData();
this.stackPanel.Children.Add(this._allShipCallsControlDict[visibleModel.Shipcall.Id]);
}
}
}));
}
#region control event handler
private async void Sc_EditRequested(ShipcallControl obj)
{
if (obj.ShipcallControlModel != null)
@ -391,6 +557,10 @@ namespace BreCalClient
}
}
#endregion
#region helper
private void ShowErrorDialog(string message, string caption)
{
Dispatcher.Invoke(new Action(() =>
@ -399,6 +569,12 @@ namespace BreCalClient
}));
}
private void EnableControlsForParticipant()
{
if (App.Participant.IsTypeFlagSet(Extensions.ParticipantType.BSMD))
this.buttonNew.Visibility = Visibility.Visible;
}
#endregion
}

View File

@ -49,5 +49,14 @@ namespace BreCalClient.Properties {
return ((string)(this["APP_TITLE"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("https://www.textbausteine.net/")]
public string LOGO_IMAGE_URL {
get {
return ((string)(this["LOGO_IMAGE_URL"]));
}
}
}
}

View File

@ -11,5 +11,8 @@
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
<Value Profile="(Default)">!!Bremen calling Testversion!!</Value>
</Setting>
<Setting Name="LOGO_IMAGE_URL" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://www.textbausteine.net/</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -0,0 +1,18 @@
# Bremen calling WPF client
## Introduction
## API / code generation
The Rest API client is generated from the OpenAPI specification [BreCalApi.yaml](../../misc/BreCalApiyaml) into the C# file [BreCalApi.cs](../../misc/BreCalApi.cs).
In order to do so an extension for Visual Studio needs to be installed: REST API Client Code Generator for VS 2022.
https://marketplace.visualstudio.com/items?itemName=ChristianResmaHelle.ApiClientCodeGenerator2022
This extension has multiple generators, for this project OpenApiCodeGenerator is used (must be set on the yaml file in the project settings).
Internally this uses Java, currently > 55 which translates into Java JDK 17 LTS.
If code generation is not working please have a look in the output pane and select appropriate output source.
## Installation
The client is deployed via ClickOnce.

View File

@ -316,6 +316,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Clear filters.
/// </summary>
public static string textClearFilters {
get {
return ResourceManager.GetString("textClearFilters", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Clear value.
/// </summary>
@ -694,6 +703,15 @@ namespace BreCalClient.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Show cancelled calls.
/// </summary>
public static string textShowCancelledShipcalls {
get {
return ResourceManager.GetString("textShowCancelledShipcalls", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sort order.
/// </summary>

View File

@ -370,4 +370,10 @@
<data name="textPasswordChanged" xml:space="preserve">
<value>Passwort geändert.</value>
</data>
<data name="textClearFilters" xml:space="preserve">
<value>Filter löschen</value>
</data>
<data name="textShowCancelledShipcalls" xml:space="preserve">
<value>Stornierte anzeigen</value>
</data>
</root>

View File

@ -199,6 +199,9 @@
<data name="textClearAssignment" xml:space="preserve">
<value>Clear assignment</value>
</data>
<data name="textClearFilters" xml:space="preserve">
<value>Clear filters</value>
</data>
<data name="textClearValue" xml:space="preserve">
<value>Clear value</value>
</data>
@ -325,6 +328,9 @@
<data name="textShipLength" xml:space="preserve">
<value>Ship length</value>
</data>
<data name="textShowCancelledShipcalls" xml:space="preserve">
<value>Show cancelled calls</value>
</data>
<data name="textSortOrder" xml:space="preserve">
<value>Sort order</value>
</data>

View File

@ -59,23 +59,23 @@
<ColumnDefinition Width="30" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<DatePicker x:Name="datePickerETAFrom" Grid.Column="0" Margin="2" />
<DatePicker x:Name="datePickerETAFrom" Grid.Column="0" Margin="2" SelectedDateChanged="datePickerETAFrom_SelectedDateChanged" SelectedDate="{Binding Path=EtaFrom}"/>
<Label Grid.Column="1" Content="{x:Static p:Resources.textTo}" />
<DatePicker x:Name="datePickerETATo" Grid.Column="2" Margin="2" />
<DatePicker x:Name="datePickerETATo" Grid.Column="2" Margin="2" SelectedDateChanged="datePickerETATo_SelectedDateChanged" SelectedDate="{Binding Path=EtaTo}"/>
</Grid>
<xctk:CheckComboBox x:Name="comboBoxCategories" Grid.Column="4" Margin="2" />
<xctk:CheckComboBox x:Name="comboBoxCategories" Grid.Column="4" Margin="2" ItemSelectionChanged="comboBoxCategories_ItemSelectionChanged" />
<Grid Grid.Column="6" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width=".5*" />
</Grid.ColumnDefinitions>
<xctk:DoubleUpDown x:Name="upDownShiplengthFrom" Grid.Column="0" Margin="2" Minimum="0" Maximum="1000" />
<xctk:DoubleUpDown x:Name="upDownShiplengthFrom" Grid.Column="0" Margin="2" Minimum="0" Maximum="1000" ValueChanged="upDownShiplengthFrom_ValueChanged" Value="{Binding Path=ShipLengthFrom}" />
<Label Grid.Column="1" Content="{x:Static p:Resources.textTo}" />
<xctk:DoubleUpDown x:Name="upDownShiplengthTo" Grid.Column="2" Margin="2" Minimum="0" Maximum="1000"/>
<xctk:DoubleUpDown x:Name="upDownShiplengthTo" Grid.Column="2" Margin="2" Minimum="0" Maximum="1000" ValueChanged="upDownShiplengthTo_ValueChanged" Value="{Binding Path=ShipLengthTo}"/>
</Grid>
<xctk:WatermarkTextBox x:Name="textBoxSearch" Grid.Column="2" Grid.Row="1" Margin="2" Watermark="{x:Static p:Resources.textEnterKeyword}" />
<xctk:CheckComboBox x:Name="comboBoxBerths" DisplayMemberPath="Name" Grid.Column="4" Grid.Row="1" Margin="2" />
<xctk:CheckComboBox x:Name="comboBoxAgencies" DisplayMemberPath="Name" Grid.Column="6" Grid.Row="1" Margin="2" />
<xctk:WatermarkTextBox x:Name="textBoxSearch" Grid.Column="2" Grid.Row="1" Margin="2" Watermark="{x:Static p:Resources.textEnterKeyword}" PreviewTextInput="textBoxSearch_PreviewTextInput" DataObject.Pasting="textBoxSearch_Pasting" TextChanged="textBoxSearch_TextChanged" />
<xctk:CheckComboBox x:Name="comboBoxBerths" DisplayMemberPath="Name" Grid.Column="4" Grid.Row="1" Margin="2" ItemSelectionChanged="comboBoxBerths_ItemSelectionChanged" />
<xctk:CheckComboBox x:Name="comboBoxAgencies" DisplayMemberPath="Name" Grid.Column="6" Grid.Row="1" Margin="2" ItemSelectionChanged="comboBoxAgencies_ItemSelectionChanged" />
</Grid>
</UserControl>

View File

@ -16,23 +16,41 @@ namespace BreCalClient
public partial class SearchFilterControl : UserControl
{
#region private fields
private SearchFilterModel _model = new();
#endregion
#region Construction
public SearchFilterControl()
{
InitializeComponent();
this.DataContext = this._model;
}
#endregion
#region events
/// <summary>
/// historically we love a clickable logo and see what will happen
/// </summary>
public event Action? LogoImageClicked;
/// <summary>
/// if the user somewhat changes the filters trigger this
/// </summary>
internal event Action? SearchFilterChanged;
#endregion
#region Properties
public string FilterAsJson { get; set; } = "";
internal SearchFilterModel SearchFilter { get { return _model; } }
#endregion
@ -48,6 +66,19 @@ namespace BreCalClient
this.comboBoxAgencies.ItemsSource = agencies;
}
public void ClearFilters()
{
this._model = new SearchFilterModel();
this.comboBoxAgencies.UnSelectAll();
this.comboBoxBerths.UnSelectAll();
this.comboBoxCategories.UnSelectAll();
this.datePickerETAFrom.SelectedDate = null;
this.datePickerETATo.SelectedDate = null;
this.textBoxSearch.Clear();
this.upDownShiplengthFrom.Value = null;
this.upDownShiplengthTo.Value = null;
}
#endregion
#region event handler
@ -62,6 +93,69 @@ namespace BreCalClient
this.comboBoxCategories.ItemsSource = Enum.GetValues(typeof(Extensions.TypeEnum));
}
private void datePickerETAFrom_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
SearchFilterChanged?.Invoke();
}
private void datePickerETATo_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
//_model.EtaTo = datePickerETATo.SelectedDate;
SearchFilterChanged?.Invoke();
}
private void comboBoxCategories_ItemSelectionChanged(object sender, Xceed.Wpf.Toolkit.Primitives.ItemSelectionChangedEventArgs e)
{
_model.Categories.Clear();
foreach(int category in comboBoxCategories.SelectedItems)
_model.Categories.Add(category);
SearchFilterChanged?.Invoke();
}
private void upDownShiplengthFrom_ValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<object> e)
{
SearchFilterChanged?.Invoke();
}
private void upDownShiplengthTo_ValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<object> e)
{
SearchFilterChanged?.Invoke();
}
private void comboBoxBerths_ItemSelectionChanged(object sender, Xceed.Wpf.Toolkit.Primitives.ItemSelectionChangedEventArgs e)
{
_model.Berths.Clear();
foreach(Berth berth in comboBoxBerths.SelectedItems)
_model.Berths.Add(berth.Id);
SearchFilterChanged?.Invoke();
}
private void comboBoxAgencies_ItemSelectionChanged(object sender, Xceed.Wpf.Toolkit.Primitives.ItemSelectionChangedEventArgs e)
{
_model.Agencies.Clear();
foreach (Participant agency in comboBoxAgencies.SelectedItems)
_model.Agencies.Add(agency.Id);
SearchFilterChanged?.Invoke();
}
private void textBoxSearch_Pasting(object sender, System.Windows.DataObjectPastingEventArgs e)
{
this.SearchFilter.SearchString = e.SourceDataObject.ToString();
SearchFilterChanged?.Invoke();
}
private void textBoxSearch_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
this.SearchFilter.SearchString = e.Text;
SearchFilterChanged?.Invoke();
}
private void textBoxSearch_TextChanged(object sender, TextChangedEventArgs e)
{
SearchFilterChanged?.Invoke();
}
#endregion
}

View File

@ -18,11 +18,11 @@ namespace BreCalClient
public DateTime? EtaTo { get; set; }
public IEnumerable<int>? Categories { get; set; }
public List<int> Categories { get; set; } = new();
public IEnumerable<Participant>? Agencies { get; set; }
public List<int> Agencies { get; set; } = new();
public IEnumerable<Berth>? Berths { get; set; }
public List<int> Berths { get; set; } = new();
public string? SearchString { get; set; }

View File

@ -42,6 +42,7 @@ namespace BreCalClient
#region Properties
public Shipcall? Shipcall { get; set; }
public Ship? Ship { get; set; }
public string? Berth { get; set; }
@ -108,6 +109,20 @@ namespace BreCalClient
return null;
}
public bool ContainsRemarkText(string text)
{
if(Shipcall != null)
{
foreach(Times times in this.Times)
{
if (times.Remarks == null) continue;
if(times.Remarks.Contains(text)) return true;
}
}
return false;
}
#endregion
#region helper