A question for Laurent and others.
I've added extension functions to the MVVM Light messenger to push and pop messages. The idea is that when a viewmodel needs to open another view/viewmodel, it would push the parameters from the message stack and then the newly opened viewmodel (or possibly the viewmodellocator) would subscribe to the message and pop the message to get the parameters. I'm looking for feedback on this idea. The code is posted below.
MVVMlightMessengerStackExtension.cs (code below has been updated since the original post to clean things up a bit and be more consistent with stack behavior)
namespace GalaSoft.MvvmLight.Messaging
{
public static class MessageHelper
{
/// <summary>
/// Store a list of all the pushed messages
/// </summary>
private static List<Tuple<int, object, object>> _q = new List<Tuple<int, object, object>>();
private static int _q_idx = Int32.MaxValue; // SL has no SortedList or SortedDictionary so keep an index the of the list to push/pops are in order
/// <summary>
/// Push a message for later retrival. Typically by a viewmodel constructor.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
/// <param name="message"></param>
/// <param name="token"></param>
[DebuggerStepThrough()]
public static void Push<TMessage>(this Messenger M, TMessage message, object token = null)
{
Monitor.Enter(_q);
try
{
_q.Add(Tuple.Create<int, object, object>(_q_idx--, message, token));
}
finally
{
Monitor.Exit(_q);
}
}
/// <summary>
/// Send a stored/delayed message
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
[DebuggerStepThrough()]
public static void PopAndSend<TMessage>(this Messenger M, object token = null)
{
TMessage mesg = M.Pop<TMessage>(token);
if (token != null)
M.Send(mesg, token);
else
M.Send(mesg);
}
/// <summary>
/// Pop a stored/delayed message
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
[DebuggerStepThrough()]
public static TMessage Pop<TMessage>(this Messenger M, object token = null)
{
Monitor.Enter(_q);
try
{
var result = _q.OrderBy(a => a.Item1).FirstOrDefault(a => a.Item2 is TMessage && a.Item3 == token);
if (result == null)
throw new InvalidOperationException("The stack is empty.");
_q.Remove(result);
return (TMessage)result.Item2;
}
finally
{
Monitor.Exit(_q);
}
}
/// <summary>
/// Peek at a stored/delayed message
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
[DebuggerStepThrough()]
public static TMessage Peek<TMessage>(this Messenger M, object token = null)
{
Monitor.Enter(_q);
try
{
var result = _q.OrderBy(a => a.Item1).FirstOrDefault(a => a.Item2 is TMessage && a.Item3 == token);
if (result == null)
throw new InvalidOperationException("The stack is empty.");
return (TMessage)result.Item2;
}
finally
{
Monitor.Exit(_q);
}
}
/// <summary>
/// Clear the stack
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
[DebuggerStepThrough()]
public static void Clear(this Messenger M)
{
Monitor.Enter(_q);
try
{
_q.Clear();
}
finally
{
Monitor.Exit(_q);
}
}
/// <summary>
/// Clear the stack
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="M"></param>
[DebuggerStepThrough()]
public static void Clear<TMessage>(this Messenger M)
{
Monitor.Enter(_q);
try
{
var delList = _q.Where(a => a.Item2 is TMessage);
foreach (var item in delList)
_q.Remove(item);
}
finally
{
Monitor.Exit(_q);
}
}
}
}
I have a somewhat similar concept in my backlog, very interesting implementation here. I will bookmark it and come back to it when the time comes.
Cheers and thanks for sharing!
Laurent
Related
3 month project about to be scrapped because of this.
Makes zero sense why this does not work.
I've tried rebooting etc. I have a sample project with the same versions of Forms etc which works.
I've tried taking the AppMapViewModelBase from that project into mine and the error still there.
Any ideas?
using System;
using System.ComponentModel;
using Prism;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace bla.Infrastructure {
public abstract class AppMapViewModelBase : BindableBase, IInitialize, INavigationAware, IConfirmNavigation, IDestructible
{
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
private int _cartItemCount;
public int CartItemCount
{
// get => _password;
get { return App.Current.CartItemCount; }
//set => SetProperty(ref _cartItemCount, value);
set
{
_cartItemCount = value;
App.Current.CartItemCount = value;
RaisePropertyChanged();
}
}
const string RootUriPrependText = "/";
DelegateCommand<string> _navigateAbsoluteCommand;
DelegateCommand<string> _navigateCommand;
DelegateCommand<string> _navigateModalCommand;
DelegateCommand<string> _navigateNonModalCommand;
private bool _isNotConnected;
public bool IsNotConnected
{
get
{
if (DesignMode.IsDesignModeEnabled)
{
return false;
}
else
{
return _isNotConnected;
}
}
set
{
SetProperty(ref _isNotConnected, value);
}
}
/// <summary>
/// Gets the navigate absolute command.
/// </summary>
/// <value>The navigate absolute command.</value>
public DelegateCommand<string> NavigateAbsoluteCommand => _navigateAbsoluteCommand ?? (_navigateAbsoluteCommand = new DelegateCommand<string>(NavigateAbsoluteCommandExecute, CanNavigateAbsoluteCommandExecute));
/// <summary>
/// Gets the navigate command.
/// </summary>
/// <value>The navigate command.</value>
public DelegateCommand<string> NavigateCommand => _navigateCommand ?? (_navigateCommand = new DelegateCommand<string>(NavigateCommandExecute, CanNavigateCommandExecute));
/// <summary>
/// Gets the navigate modal command.
/// </summary>
/// <value>The navigate modal command.</value>
public DelegateCommand<string> NavigateModalCommand => _navigateModalCommand ?? (_navigateModalCommand = new DelegateCommand<string>(NavigateModalCommandExecute, CanNavigateModalCommandExecute));
/// <summary>
/// Gets the navigate non modal command.
/// </summary>
/// <value>The navigate non modal command.</value>
public DelegateCommand<string> NavigateNonModalCommand => _navigateNonModalCommand ?? (_navigateNonModalCommand = new DelegateCommand<string>(NavigateNonModalCommandExecute, CanNavigateNonModalCommandExecute));
/// <summary>
/// Gets the navigation service.
/// </summary>
/// <value>The navigation service.</value>
protected INavigationService NavigationService { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AppMapViewModelBase"/> class.
/// </summary>
/// <param name="navigationService">The navigation service.</param>
/// <exception cref="System.ArgumentNullException">navigationService</exception>
protected AppMapViewModelBase(INavigationService navigationService)
{
if (navigationService == null)
{
throw new ArgumentNullException(nameof(navigationService));
}
this.NavigationService = navigationService;
Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged;
IsNotConnected = Connectivity.NetworkAccess != NetworkAccess.Internet;
}
void Connectivity_ConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
{
if (e.NetworkAccess != NetworkAccess.Internet)
{
//NavigationService.NavigateAsync("NavigationPage/NoInternet");
}
IsNotConnected = e.NetworkAccess != NetworkAccess.Internet;
}
/// <summary>
/// Determines whether this instance accepts being navigated away from. This method is invoked by Prism before a navigation operation and is a member of IConfirmNavigation.
/// </summary>
/// <param name="parameters">The navigation parameters.</param>
/// <returns><c>True</c> if navigation can continue, <c>False</c> if navigation is not allowed to continue</returns>
public virtual bool CanNavigate(INavigationParameters parameters)
{
return true;
}
/// <summary>
/// Determines whether this instance can execute the NavigateAbsoluteCommand.
/// </summary>
/// <param name="uri">The uri.</param>
/// <returns><c>true</c> if this instance can execute NavigateAbsoluteCommand; otherwise, <c>false</c>.</returns>
protected virtual bool CanNavigateAbsoluteCommandExecute(string uri)
{
return !String.IsNullOrEmpty(uri);
}
/// <summary>
/// Determines whether this instance can execute the NavigateAbsoluteCommand.
/// </summary>
/// <param name="uri">The uri.</param>
/// <returns><c>true</c> if this instance can execute NavigateAbsoluteCommand; otherwise, <c>false</c>.</returns>
protected virtual bool CanNavigateCommandExecute(string uri)
{
return !String.IsNullOrEmpty(uri);
}
/// <summary>
/// Determines whether this instance can execute the NavigateModalCommand.
/// </summary>
/// <param name="uri">The uri.</param>
/// <returns><c>true</c> if this instance can execute NavigateModalCommand; otherwise, <c>false</c>.</returns>
protected virtual bool CanNavigateModalCommandExecute(string uri)
{
return !String.IsNullOrEmpty(uri);
}
/// <summary>
/// Determines whether this instance can execute the NavigateNonModalCommand.
/// </summary>
/// <param name="uri">The uri.</param>
/// <returns><c>true</c> if this instance can execute NavigateNonModalCommand; otherwise, <c>false</c>.</returns>
protected virtual bool CanNavigateNonModalCommandExecute(string uri)
{
return !String.IsNullOrEmpty(uri);
}
/// <summary>
/// <p>Invoked by Prism Navigation when the instance is removed from the navigation stack.</p>
/// <p>Deriving class can override and perform any required clean up.</p>
/// </summary>
public virtual void Destroy()
{
}
/// <summary>
/// Navigates to the uri after creating a new navigation root. (Effectively replacing the Application MainPage.)
/// </summary>
/// <param name="uri">The uri text.</param>
/// <returns>Task.</returns>
protected virtual async void NavigateAbsoluteCommandExecute(string uri)
{
if (!CanNavigateAbsoluteCommandExecute(uri))
{
return;
}
if (!uri.StartsWith(RootUriPrependText))
{
uri = string.Concat(RootUriPrependText, uri);
}
await this.NavigationService.NavigateAsync(uri, null, false,false);
}
/// <summary>
/// Navigates to the uri.
/// </summary>
/// <param name="uri">The uri text.</param>
/// <returns>Task.</returns>
protected virtual async void NavigateCommandExecute(string uri)
{
if (!CanNavigateCommandExecute(uri))
{
return;
}
await this.NavigationService.NavigateAsync(uri);
}
/// <summary>
/// Navigates to the uri using a Modal navigation.
/// </summary>
/// <param name="uri">The uri text.</param>
/// <returns>Task.</returns>
protected virtual async void NavigateModalCommandExecute(string uri)
{
if (!CanNavigateModalCommandExecute(uri))
{
return;
}
await this.NavigationService.NavigateAsync(uri,null, useModalNavigation: true,animated:false);
}
/// <summary>
/// Navigates to the uri using Non-Modal navigation.
/// </summary>
/// <param name="uri">The uri text.</param>
/// <returns>Task.</returns>
protected virtual async void NavigateNonModalCommandExecute(string uri)
{
if (!CanNavigateNonModalCommandExecute(uri))
{
return;
}
await this.NavigationService.NavigateAsync(uri,null, useModalNavigation: false,false);
}
/// <summary>
/// Invoked by Prism immediately after the ViewModel has been created.
/// </summary>
/// <param name="parameters">The parameters.</param>
public virtual void InitializeAsync(INavigationParameters parameters)
{
}
/// <summary>
/// Invoked by Prism after navigating away from viewmodel's page.
/// </summary>
/// <param name="parameters">The parameters.</param>
public virtual void OnNavigatedFrom(INavigationParameters parameters)
{
}
/// <summary>
/// Invoked by Prism after navigating to the viewmodel's page.
/// </summary>
/// <param name="parameters">The parameters.</param>
public virtual void OnNavigatedTo(INavigationParameters parameters)
{
}
public virtual void Initialize(INavigationParameters parameters)
{
}
}
}
The signature of the method is wrong.
The prefix I is missing before NavigationParameters, because OnNavigatedFrom expects an interface of type INavigationParameters as parameter:
Change NavigationParameters to INavigationParameters
public override void OnNavigatedFrom (INavigationParameters navigationParameters)
{
}
I need to create an app thats basically functions like a multi-user realtime questionnaire or trivia.
I've gotten to the point where i've created a questionnaire and associated users to it, but i'm hitting a road block on how to go forward.
Basically what i need to do next is
For each question in the questionnaire:
1) have the server send the users a question
2) wait for all of the users to respond or a set timeout period
3) display to the users a question result page
I was wondering how i should attempt to do something like this. Any help or resources would be wonderful
I am building a chat application that has something very similar using Server Side Blazor.
The way I did was I inject a class I created called SubscriberService:
#inject Services.SubscriberService SubscriberService
Then in my ConfigureServices method in Startup.cs I add this:
services.AddSingleton<SubscriberService>();
The Add Singleton means only 1 instance will be created for all your browser instances (users).
This makes my Subscriber Services available to all my subcribers, which is just a name, Guid Id and a callback delegate
#region using statements
using DataJuggler.UltimateHelper.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Transactions;
#endregion
namespace BlazorChat.Services
{
#region class SubscriberService
/// <summary>
/// This class is used to subscribe to services, so other windows get a notification a new message
/// came in.
/// </summary>
public class SubscriberService
{
#region Private Variables
private int count;
private Guid serverId;
private List<SubscriberCallback> subscribers;
#endregion
#region Constructor
/// <summary>
/// Create a new instance of a 'SubscriberService' object.
/// </summary>
public SubscriberService()
{
// Create a new Guid
this.ServerId = Guid.NewGuid();
Subscribers = new List<SubscriberCallback>();
}
#endregion
#region Methods
#region BroadcastMessage(SubscriberMessage message)
/// <summary>
/// This method Broadcasts a Message to everyone that ins't blocked.
/// Note To Self: Add Blocked Feature
/// </summary>
public void BroadcastMessage(SubscriberMessage message)
{
// if the value for HasSubscribers is true
if ((HasSubscribers) && (NullHelper.Exists(message)))
{
// Iterate the collection of SubscriberCallback objects
foreach (SubscriberCallback subscriber in Subscribers)
{
// if the Callback exists
if ((subscriber.HasCallback) && (subscriber.Id != message.FromId))
{
// to do: Add if not blocked
// send the message
subscriber.Callback(message);
}
}
}
}
#endregion
#region GetSubscriberNames()
/// <summary>
/// This method returns a list of Subscriber Names ()
/// </summary>
public List<string> GetSubscriberNames()
{
// initial value
List<string> subscriberNames = null;
// if the value for HasSubscribers is true
if (HasSubscribers)
{
// create the return value
subscriberNames = new List<string>();
// Get the SubscriberNamesl in alphabetical order
List<SubscriberCallback> sortedNames = Subscribers.OrderBy(x => x.Name).ToList();
// Iterate the collection of SubscriberService objects
foreach (SubscriberCallback subscriber in sortedNames)
{
// Add this name
subscriberNames.Add(subscriber.Name);
}
}
// return value
return subscriberNames;
}
#endregion
#region Subscribe(string subscriberName)
/// <summary>
/// method returns a message with their id
/// </summary>
public SubscriberMessage Subscribe(SubscriberCallback subscriber)
{
// initial value
SubscriberMessage message = null;
// If the subscriber object exists
if ((NullHelper.Exists(subscriber)) && (HasSubscribers))
{
// Add this item
Subscribers.Add(subscriber);
// return a test message for now
message = new SubscriberMessage();
// set the message return properties
message.FromName = "Subscriber Service";
message.FromId = ServerId;
message.ToName = subscriber.Name;
message.ToId = subscriber.Id;
message.Data = Subscribers.Count.ToString();
message.Text = "Subscribed";
}
// return value
return message;
}
#endregion
#region Unsubscribe(Guid id)
/// <summary>
/// This method Unsubscribe
/// </summary>
public void Unsubscribe(Guid id)
{
// if the value for HasSubscribers is true
if ((HasSubscribers) && (Subscribers.Count > 0))
{
// attempt to find this callback
SubscriberCallback callback = Subscribers.FirstOrDefault(x => x.Id == id);
// If the callback object exists
if (NullHelper.Exists(callback))
{
// Remove this item
Subscribers.Remove(callback);
// create a new message
SubscriberMessage message = new SubscriberMessage();
// set the message return properties
message.FromId = ServerId;
message.FromName = "Subscriber Service";
message.Text = callback.Name + " has left the conversation.";
message.ToId = Guid.Empty;
message.ToName = "Room";
// Broadcast the message to everyone
BroadcastMessage(message);
}
}
}
#endregion
#endregion
#region Properties
#region Count
/// <summary>
/// This property gets or sets the value for 'Count'.
/// </summary>
public int Count
{
get { return count; }
set { count = value; }
}
#endregion
#region HasSubscribers
/// <summary>
/// This property returns true if this object has a 'Subscribers'.
/// </summary>
public bool HasSubscribers
{
get
{
// initial value
bool hasSubscribers = (this.Subscribers != null);
// return value
return hasSubscribers;
}
}
#endregion
#region ServerId
/// <summary>
/// This property gets or sets the value for 'ServerId'.
/// </summary>
public Guid ServerId
{
get { return serverId; }
set { serverId = value; }
}
#endregion
#region Subscribers
/// <summary>
/// This property gets or sets the value for 'Subscribers'.
/// </summary>
public List<SubscriberCallback> Subscribers
{
get { return subscribers; }
set { subscribers = value; }
}
#endregion
#endregion
}
#endregion
}
Here is my SubscriberCallback.cs:
#region using statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
#endregion
namespace BlazorChat
{
#region class SubscriberCallback
/// <summary>
/// This class is used to register a subscriber with the ChatService
/// </summary>
public class SubscriberCallback
{
#region Private Variables
private string name;
private Guid id;
private Callback callback;
private List<Guid> blockedList;
#endregion
#region Constructor
/// <summary>
/// Create a new instance of a SubscriberCallback instance
/// </summary>
public SubscriberCallback(string name)
{
// store the Name
Name = name;
// Create the Id
Id = Guid.NewGuid();
// create a BlockedList
BlockedList = new List<Guid>();
}
#endregion
#region Methods
#region ToString()
/// <summary>
/// This method is used to return the Name of the Subscriber when ToString is called.
/// </summary>
/// <returns></returns>
public override string ToString()
{
// return the Name when ToString is called
return this.Name;
}
#endregion
#endregion
#region Properties
#region BlockedList
/// <summary>
/// This property gets or sets the value for 'BlockedList'.
/// </summary>
public List<Guid> BlockedList
{
get { return blockedList; }
set { blockedList = value; }
}
#endregion
#region Callback
/// <summary>
/// This property gets or sets the value for 'Callback'.
/// </summary>
public Callback Callback
{
get { return callback; }
set { callback = value; }
}
#endregion
#region HasBlockedList
/// <summary>
/// This property returns true if this object has a 'BlockedList'.
/// </summary>
public bool HasBlockedList
{
get
{
// initial value
bool hasBlockedList = (this.BlockedList != null);
// return value
return hasBlockedList;
}
}
#endregion
#region HasCallback
/// <summary>
/// This property returns true if this object has a 'Callback'.
/// </summary>
public bool HasCallback
{
get
{
// initial value
bool hasCallback = (this.Callback != null);
// return value
return hasCallback;
}
}
#endregion
#region HasName
/// <summary>
/// This property returns true if the 'Name' exists.
/// </summary>
public bool HasName
{
get
{
// initial value
bool hasName = (!String.IsNullOrEmpty(this.Name));
// return value
return hasName;
}
}
#endregion
#region Id
/// <summary>
/// This property gets or sets the value for 'Id'.
/// </summary>
public Guid Id
{
get { return id; }
set { id = value; }
}
#endregion
#region Name
/// <summary>
/// This property gets or sets the value for 'Name'.
/// </summary>
public string Name
{
get { return name; }
set { name = value; }
}
#endregion
#endregion
}
#endregion
}
And here is my delegate class:
/// <summary>
/// This delegate is used by the SubscriberService to send messages to any subscribers
/// </summary>
/// <returns></returns>
public delegate void Callback(SubscriberMessage message);
Then in my component I call methods like this:
// Send this message to all clients
SubscriberService.BroadcastMessage(message);
And each client has a listen method:
SubscriberCallback callback = new SubscriberCallback(SubscriberName);
callback.Callback = Listen;
callback.Name = SubscriberName;
// Get a message back
SubscriberMessage message = SubscriberService.Subscribe(callback);
Here is my Listen method, it just waits for messages;
using DataJuggler.UltimateHelper.Core; // Nuget package
public void Listen(SubscriberMessage message)
{
// if the message exists (part of DataJuggler.UltimateHelper.Core Nuget Package)
// Same as (message != null)
if (NullHelper.Exists(message))
{
// if the message contains Joined the conversation
if ((message.Text.Contains("joined the conversation")) ||
(message.Text.Contains("left the conversation")))
{
// this updates my list of 'Whose On' whenever a user joins or leaves
// Get the Names again
this.Names = SubscriberService.GetSubscriberNames();
// Update the UI
Refresh();
}
else
{
// my display message code is here
}
}
And finally here is my Subscriber message:
#region using statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#endregion
namespace BlazorChat
{
#region class SubscriberMessage
/// <summary>
/// This class is used to send information between components / pages.
/// </summary>
public class SubscriberMessage
{
#region Private Variables
private string text;
private Guid fromId;
private Guid toId;
private string fromName;
private string toName;
private object data;
private string valid;
private DateTime sent;
private string invalidReason;
#endregion
#region Properties
#region Data
/// <summary>
/// This property gets or sets the value for 'Data'.
/// </summary>
public object Data
{
get { return data; }
set { data = value; }
}
#endregion
#region FromId
/// <summary>
/// This property gets or sets the value for 'FromId'.
/// </summary>
public Guid FromId
{
get { return fromId; }
set { fromId = value; }
}
#endregion
#region FromName
/// <summary>
/// This property gets or sets the value for 'FromName'.
/// </summary>
public string FromName
{
get { return fromName; }
set { fromName = value; }
}
#endregion
#region HasText
/// <summary>
/// This property returns true if the 'Text' exists.
/// </summary>
public bool HasText
{
get
{
// initial value
bool hasText = (!String.IsNullOrEmpty(this.Text));
// return value
return hasText;
}
}
#endregion
#region InvalidReason
/// <summary>
/// This property gets or sets the value for 'InvalidReason'.
/// </summary>
public string InvalidReason
{
get { return invalidReason; }
set { invalidReason = value; }
}
#endregion
#region Sent
/// <summary>
/// This property gets or sets the value for 'Sent'.
/// </summary>
public DateTime Sent
{
get { return sent; }
set { sent = value; }
}
#endregion
#region Text
/// <summary>
/// This property gets or sets the value for 'Text'.
/// </summary>
public string Text
{
get { return text; }
set { text = value; }
}
#endregion
#region ToId
/// <summary>
/// This property gets or sets the value for 'ToId'.
/// </summary>
public Guid ToId
{
get { return toId; }
set { toId = value; }
}
#endregion
#region ToName
/// <summary>
/// This property gets or sets the value for 'ToName'.
/// </summary>
public string ToName
{
get { return toName; }
set { toName = value; }
}
#endregion
#region Valid
/// <summary>
/// This property gets or sets the value for 'Valid'.
/// </summary>
public string Valid
{
get { return valid; }
set { valid = value; }
}
#endregion
#endregion
}
#endregion
}
BlazorChat is a sample project I am still working on as part of my Nuget package:
DataJuggler.Blazor.Components, which contains a Sprite component, ProgressBar and Validation component.
The full code is here in the Samples folder of this project if I left anything out.
https://github.com/DataJuggler/DataJuggler.Blazor.Components
Documentation and SQL scripts are missing, so sorry, its a work in progress.
While working on a Asp.Net Core Web Api I tried to make my Domain Model as DRY as possible.Therefore I first followed this link to create a Base Entity with all the Fields I knew I would need in my Application (I pretty much pasted the code so I am not going to paste it here again). After working a little more I wanted to add File Uploading to my project. To Realize that I created two Classes called Photo and TextFile:
public class TextFile : File
{
#region Members
/// <summary>
/// The ForeignKey to the User
/// </summary>
public Guid UserId { get; private set; }
/// <summary>
/// The NavigationProperty to the User that added this Photo.
/// </summary>
public virtual User User { get; set; }
#endregion
#region Constructors
/// <summary>
/// For Ef Core
/// </summary>
private TextFile()
{ }
/// <summary>
/// Creates a new Instance of a TextFile.
/// </summary>
/// <param name="userId">The Id of the User that created this TextFile</param>
public TextFile(Guid userId)
{
UserId = userId;
}
#endregion
}
/// <summary>
/// Represents a Photo that got Uploaded
/// </summary>
public class Photo : File
{
#region Members
/// <summary>
/// Determines where this Image gets shown.
/// </summary>
public ImageOption? Option { get; private set; }
/// <summary>
/// The ForeignKey to the User
/// </summary>
public Guid UserId { get; private set; }
/// <summary>
/// The NavigationProperty to the User that added this Photo.
/// </summary>
public virtual User User { get; set; }
#endregion
#region Constructors
/// <summary>
/// For EF Core
/// </summary>
private Photo()
{ }
/// <summary>
/// Basic Constructor
/// </summary>
/// <param name="userId"></param>
public Photo(Guid userId)
{
UserId = userId;
}
#endregion
#region Methods
/// <summary>
/// Sets the Image Option only once
/// </summary>
/// <param name="option"></param>
public void SetImageOption(ImageOption option)
{
if (Option.HasValue)
return;
else
Option = option;
}
#endregion
}
Here I created a abstract class called File because I wanted to avoid repeating myself with the same Fields and Methods.The File class inherits from Entity (from the article above) and has common Fiels like FileName and Filesize:
/// <summary>
/// Base Class for all Files
/// </summary>
public abstract class File : Entity<Guid>
{
#region Members
/// <summary>
/// The name of the File
/// </summary>
public string FileName { get; private set; }
/// <summary>
/// The Path to the File
/// </summary>
public string FilePath { get; private set; }
/// <summary>
/// The Size of the File
/// </summary>
public int FileSize { get; private set; }
public FileExtension Extension { get; private set; }
#endregion
#region Methods
/// <summary>
/// Creates a new Text File to be uploaded to the Database.
/// </summary>
/// <param name="file">The File to be Uploaded</param>
/// <param name="relativeFolderPath">The Relative Path from the WebRoot.</param>
/// <param name="userId">A UserId</param>
/// <param name="extension">The Extension of this File.</param>
/// <param name="token">A CancellationToken</param>
/// <returns></returns>
public static File CreateTextFile(IFormFile file, string relativeFolderPath, Guid userId, FileExtension extension, CancellationToken token)
{
token.ThrowIfCancellationRequested();
var textFile = new TextFile(userId);
textFile.SetFileProperties(file, relativeFolderPath, extension, token);
return textFile;
}
/// <summary>
/// Creates a new Image File Model
/// </summary>
/// <param name="file">The File to be uploaded</param>
/// <param name="relativeFolderPath">The relative Path to the Folder this Image resides in.</param>
/// <param name="userId">A UserId</param>
/// <param name="extension">The File Extension</param>
/// <param name="token">A CancellationToken</param>
/// <returns></returns>
public static File CreatePhoto(IFormFile file, string relativeFolderPath, Guid userId, FileExtension extension, CancellationToken token)
{
token.ThrowIfCancellationRequested();
var photo = new Photo(userId);
photo.SetFileProperties(file, relativeFolderPath, extension, token);
return photo;
}
/// <summary>
/// Set Properties on File Entity
/// </summary>
/// <param name="file">The File</param>
/// <param name="relativeFolderPath">The Path extending from the WebRoot</param>
/// <param name="extension">The File Extension</param>
/// <param name="token">A CancellationToken</param>
private void SetFileProperties(IFormFile file, string relativeFolderPath, FileExtension extension, CancellationToken token)
{
if(file == null)
throw new ArgumentNullException(nameof(file));
if(string.IsNullOrWhiteSpace(relativeFolderPath))
throw new ArgumentNullException(nameof(relativeFolderPath));
token.ThrowIfCancellationRequested();
FileSize = (int) file.Length;
Extension = extension;
FileName = Guid.NewGuid() + "." + extension.ToString().ToLower();
FilePath = Path.Combine(relativeFolderPath, FileName);
}
/// <summary>
/// Sets the Extension of this File
/// </summary>
/// <param name="extension"></param>
/// <param name="ext">The Extension of the File</param>
/// <param name="token">A CancellationToken</param>
private static void FindExtension(string extension, out FileExtension ext, CancellationToken token)
{
token.ThrowIfCancellationRequested();
switch (extension.ToLower())
{
case ".jpg":
ext = FileExtension.Jpg;
break;
case ".jpeg":
ext = FileExtension.Jpeg;
break;
case ".png":
ext = FileExtension.Png;
break;
case ".bmp":
ext = FileExtension.Bmp;
break;
case ".gif":
ext = FileExtension.Gif;
break;
case ".tif":
ext = FileExtension.Tif;
break;
case ".tiff":
ext = FileExtension.Tiff;
break;
case ".svg":
ext = FileExtension.Svg;
break;
case ".doc":
ext = FileExtension.Doc;
break;
case ".docx":
ext = FileExtension.Docx;
break;
case ".odt":
ext = FileExtension.Odt;
break;
case ".rtf":
ext = FileExtension.Rtf;
break;
case ".txt":
ext = FileExtension.Txt;
break;
case "xls":
ext = FileExtension.Xls;
break;
case ".xlsx":
ext = FileExtension.Xlsx;
break;
case ".ppt":
ext = FileExtension.Ppt;
break;
case ".pptx":
ext = FileExtension.Pptx;
break;
case ".pdf":
ext = FileExtension.Pdf;
break;
default:
throw new InvalidFileExtensionException($"The Extension {extension.ToLower()} is not allowed.");
}
}
/// <summary>
/// Determines if the Specified Extension is a allowed Extension.
/// Returns true in case the extension is a file extension.
/// Returns False in case the Extension is a Image File.
/// The FileExtension Parameter is always set
/// </summary>
/// <param name="extensionName">The extension as a string</param>
/// <param name="extension">The Extension that this File has.</param>
/// <param name="token">A CancellationToken</param>
/// <returns></returns>
public static bool IsTextFile(string extensionName, out FileExtension extension, CancellationToken token)
{
token.ThrowIfCancellationRequested();
FindExtension(extensionName, out extension, token);
return (int) extension > 8;
}
/// <summary>
/// Determines if the Extension is a allowed Extension and a Image File.
/// the Extension will always be set.
/// </summary>
/// <param name="extensionName">The Extension as string</param>
/// <param name="extension">The FileExtension</param>
/// <param name="token">A CancellationToken</param>
/// <returns></returns>
public static bool IsImageFile(string extensionName, out FileExtension extension, CancellationToken token)
{
token.ThrowIfCancellationRequested();
FindExtension(extensionName, out extension, token);
return (int)extension < 8;
}
#endregion
}
And here is where my dilemma starts: When I try to apply a Code First Migration I get the following in the Migration:
migrationBuilder.CreateTable(
name: "Files",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
Created = table.Column<DateTime>(nullable: true),
LastModified = table.Column<DateTime>(nullable: true),
FileName = table.Column<string>(nullable: true),
FilePath = table.Column<string>(nullable: true),
FileSize = table.Column<int>(nullable: false),
Extension = table.Column<int>(nullable: false),
UserId = table.Column<Guid>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Files", x => x.Id);
table.ForeignKey(
name: "FK_Files_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Files_UserId",
table: "Files",
column: "UserId")
That should not happen as I only want to have my derived classes as Tables and not my Base Class. I already tried resolving it with the Ignore on Modelbuilder in my OnModelCreating:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration(new UserRoleConfiguration());
builder.ApplyConfiguration(new UserTokenConfiguration());
builder.Ignore<File>();
builder.Entity<User>().OwnsOne(x => x.FullName, fullName =>
{
fullName.OwnsOne(x => x.FirstName, firstName =>
{
firstName.Property(p => p.FirstNamePart).HasColumnName("FirstName_FirstPart").HasMaxLength(255)
.IsRequired();
firstName.Property(p => p.NameSeperator).HasColumnName("FirstName_NameSeperator").HasMaxLength(5);
firstName.Property(p => p.LastNamePart).HasColumnName("FirstName_LastPart").HasMaxLength(255);
});
fullName.OwnsOne(x => x.LastName, lastName =>
{
lastName.Property(p => p.FirstNamePart).HasColumnName("LastName_FirstPart").HasMaxLength(255)
.IsRequired();
lastName.Property(p => p.NameSeperator).HasColumnName("LastName_NameSeperator").HasMaxLength(5);
lastName.Property(p => p.LastNamePart).HasColumnName("LastName_LastPart").HasMaxLength(255);
});
});
builder.ApplyAllConfigurations();
}
But the Output is still the same. Therefore I am asking how to resolve this, so I can have only the derived classes in the Database and not the abstract class.
I'm posting the Code you asked for here:
/// <summary>
/// The User of this Application.
/// </summary>
public class User : Entity<Guid>
{
/// <summary>
/// Basic Constructor for the User
/// </summary>
public User()
{
UserRoles = new HashSet<UserRole>();
UserClaims = new HashSet<UserClaim>();
Tokens = new HashSet<UserToken>();
Photos = new HashSet<Photo>();
Files = new HashSet<TextFile>();
}
/// <summary>
/// A Concurrency Stamp
/// </summary>
public string ConcurrencyStamp { get; set; }
/// <summary>
/// The Email of this User
/// </summary>
public string Email { get; set; }
/// <summary>
/// The Normalized Email of this User
/// </summary>
public string NormalizedEmail { get; set; }
/// <summary>
/// Flag that indicates if the User has Confirmed his Email.
/// </summary>
public bool EmailConfirmed { get; set; }
/// <summary>
/// The User Name of this User.
/// </summary>
public string Username { get; set; }
/// <summary>
/// The normalized User Name
/// </summary>
public string NormalizedUsername { get; set; }
/// <summary>
/// The hashed and salted Password.
/// </summary>
public string PasswordHash { get; set; }
/// <summary>
/// A Security Stamp to validate The Users Information
/// </summary>
public string SecurityStamp { get; set; }
/// <summary>
/// The Full Name of a User.
/// </summary>
public FullName FullName { get; set; }
/// <summary>
/// The specific Y-Number that identifies the User
/// </summary>
public string YNumberId { get; set; }
/// <summary>
/// The YNumber of this User.
/// </summary>
public YNumber YNumber { get; set; }
/// <summary>
/// The Collection of Roles.
/// </summary>
public virtual ICollection<UserRole> UserRoles { get; }
/// <summary>
/// The Collection of User Claims.
/// </summary>
public virtual ICollection<UserClaim> UserClaims { get; }
public virtual ICollection<UserToken> Tokens { get; }
public virtual ICollection<Photo> Photos { get; }
public virtual ICollection<TextFile> Files { get; }
}
/// <summary>
/// Applies all Configurations in this Assembly to the specified ModelBuilder Instance.
/// </summary>
/// <param name="modelBuilder">The Instance of the ModelBuilder that configures the Database.</param>
public static void ApplyAllConfigurations(this ModelBuilder modelBuilder)
{
var applyConfigurationMethodInfo = modelBuilder
.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.First(method => method
.Name
.Equals("ApplyConfiguration", StringComparison.OrdinalIgnoreCase));
var ret = typeof(ApplicationDbContext)
.Assembly
.GetTypes()
.Select(type =>
(type, i: type
.GetInterfaces()
.FirstOrDefault(i => i
.Name
.Equals(typeof(IEntityTypeConfiguration<>)
.Name, StringComparison.OrdinalIgnoreCase))))
.Where(it => it.i != null)
.Select(it => (et: it.i.GetGenericArguments()[0], configObject: Activator.CreateInstance(it.Item1)))
.Select(it =>
applyConfigurationMethodInfo.MakeGenericMethod(it.et)
.Invoke(modelBuilder, new[] {it.configObject}));
}
While checking my Code I had the Feeling that the error is not directly in the migration but in the Method that applies my Configuration. I am thinking that because the TextFile Class is not in the Db (I am trying to add it with this migration) and the Files Table that should be created has exaclty all Fiels of the TextFile Class. It is only named wrong. My Configuration for the TextFile class looks like this:
public class TextFileConfiguration : IEntityTypeConfiguration<TextFile>
{
public void Configure(EntityTypeBuilder<TextFile> builder)
{
//Set Primary Key
builder
.HasKey(x => x.Id);
//Add ValueGeneration
builder
.Property(x => x.Id)
.UseSqlServerIdentityColumn();
//Set Table Name
builder
.ToTable("TextFiles");
//Make Filename Required with MaxLength of 50 (because filename = Guid + FileExtension)
builder
.Property(x => x.FileName)
.IsRequired()
.HasMaxLength(50);
//Configure Inverse Navigation Property.
builder
.HasOne(x => x.User)
.WithMany(y => y.Files)
.HasForeignKey(z => z.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Could it be that my Configuration just doesn't get applied?
Well well well,
now after punishing myself for not reading my previous migrations precisely enough I am back to answering my Question:
I tried adding the following line directly into OnModelCreating:
builder.Entity<TextFile>().ToTable("Text Files");
and magic:
migrationBuilder.CreateTable(
name: "Text Files",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
Created = table.Column<DateTime>(nullable: true),
LastModified = table.Column<DateTime>(nullable: true),
FileName = table.Column<string>(nullable: true),
FilePath = table.Column<string>(nullable: true),
FileSize = table.Column<int>(nullable: false),
Extension = table.Column<int>(nullable: false),
UserId = table.Column<Guid>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Files", x => x.Id);
table.ForeignKey(
name: "FK_Files_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Files_UserId",
table: "Files",
column: "UserId")
The Migration was right. Therefore I checked my ApplyConfiguration and realized it isn't applying any Configuration at all. Therefore I changed it to:
var implementedConfigTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => !t.IsAbstract
&& !t.IsGenericTypeDefinition
&& t.GetTypeInfo().ImplementedInterfaces.Any(i =>
i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)));
foreach (var configType in implementedConfigTypes)
{
dynamic config = Activator.CreateInstance(configType);
modelBuilder.ApplyConfiguration(config);
}
Courtesy of this Question on SO
And thats applying now all Configurations, meaning I get the Correct Name and Amount of Fields for all Tables.
Thank you Ivan Stoev for trying to figure it out. Your Comment gave me the right hint.
I have a form that allow to the user to update his profile. When the form is submitted an ajax request is raised:
$.ajax({
url: url,
data: formData,
type: 'POST',
success: function (response) {
alert(true);
},
error: function (jqXHR, textStatus, errorThrown) {
//Handle error
}
});
inside the ajax request I need to check if an error happened, if yes, based on the generated error I want display a different exception message.
Now the main problem is that the method called return a ViewModel of the updated user, something like:
publi class UserController : Controller
{
private readonly IUserRepository _repo;
public UserController(IUserRepository repo)
{
_repo = repo;
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateUser(UserProfileViewModel updateUser)
{
if(ModelState.IsValid)
{
updateUser = await _repo.UpdateUserAsync(updateUser);
}
return RedirectToAction("Profile");
}
The controller have a dependency injection of IUserRepository which actually handle the logic to update the user, eg:
public async Task<User> UpdateUserAsync(UserProfileViewModel updatedUser)
{
if(updatedUser.FirstName == "")
throw new Exception("FirstName not filled");
}
as you can see from the example above, if the FirstName is not filled, then an exception is thrown.
I want avoid the use of the exception; after some research I found BadRequest(), the problem is that BadRequest seems missing from AspNetCore, seems only available in the API version.
Someone have a good way to manage that?
#poke and #agua from mars are completely right; you should use model validation. If, however, your validation is a bit more complex and you have to handle it in your service, you can use this pattern. First, create a class that represents not just the data, but an indicator of the service's success in getting the data.
public class Result<T>
{
public bool HasError { get; set; }
public T Data { get; set; }
}
This above is very simplistic; you'd likely want more information.
Then, in your service, change the signature to return a Result<T> and add code to create the appropriate one:
public async Task<Result<User>> UpdateUserAsync(UserProfileViewModel updatedUser) {
if (updatedUser.FirstName == "")
return new Result<User> {
HadError = true,
Data = (User)null
};
// do something to save
return new Result<User> {
HadError = false,
Data = updatedUser
};
}
And then update your action to receive the Result<T> and return the appropriate result:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateUser(UserProfileViewModel updateUser) {
var result = new Result<User> {
HadError = true,
Data = (User) null
};
if (ModelState.IsValid) {
result = await _repo.UpdateUserAsync(updateUser);
}
return result.HadError ? BadRequest() : RedirectToAction("Profile");
}
Again, for something like a required property, there are much better methods. I've found the above pattern useful when errors occur in the service and I want to communicate that fact to the controller/action/UI.
As poke told you, use can use model validation by decorating your UserProfileViewModel.FirstName with Required attribute :
public class UserProfileViewModel
{
[Required]
public string Name { get; set; }
}
I add filters to my configuration to factorize the code.
One to check model:
/// <summary>
/// Check api model state filter
/// </summary>
public class ApiCheckModelStateFilter : IActionFilter
{
private readonly PathString _apiPathString = PathString.FromUriComponent("/api");
/// <summary>
/// Called after the action executes, before the action result.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext" />.</param>
public void OnActionExecuted(ActionExecutedContext context)
{
}
/// <summary>
/// Called before the action executes, after model binding is complete.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
/// <exception cref="InvalidOperationException"></exception>
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.HttpContext.Request.Path.StartsWithSegments(_apiPathString))
{
return;
}
var state = context.ModelState;
if (!state.IsValid)
{
var message = string.Join("; ", state.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
throw new InvalidOperationException(message);
}
}
}
And another one to manage status code depending on exception:
/// <summary>
/// Api exception filter
/// </summary>
public class ApiExceptionFilter : IExceptionFilter, IAsyncExceptionFilter
{
private readonly PathString _apiPathString = PathString.FromUriComponent("/api");
private readonly ILogger<ApiExceptionFilter> _logger;
/// <summary>
/// Initialize a new instance of <see cref="ApiExceptionFilter"/>
/// </summary>
/// <param name="logger">A logger</param>
public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
{
_logger = logger;
}
/// <summary>
/// Called after an action has thrown an <see cref="T:System.Exception" />.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
/// <returns>
/// A <see cref="T:System.Threading.Tasks.Task" /> that on completion indicates the filter has executed.
/// </returns>
public Task OnExceptionAsync(ExceptionContext context)
{
Process(context);
return Task.CompletedTask;
}
/// <summary>
/// Called after an action has thrown an <see cref="T:System.Exception" />.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
public void OnException(ExceptionContext context)
{
Process(context);
}
private void Process(ExceptionContext context)
{
var e = context.Exception;
_logger.LogError(e, e.Message);
if (!context.HttpContext.Request.Path.StartsWithSegments(_apiPathString))
{
return;
}
else if (e is EntityNotFoundException)
{
context.Result = WriteError(HttpStatusCode.NotFound, e);
}
else if (e is InvalidOperationException)
{
context.Result = WriteError(HttpStatusCode.BadRequest, e);
}
else if (e.GetType().Namespace == "Microsoft.EntityFrameworkCore")
{
context.Result = WriteError(HttpStatusCode.BadRequest, e);
}
else
{
context.Result = WriteError(HttpStatusCode.InternalServerError, e);
}
}
private IActionResult WriteError(HttpStatusCode statusCode, Exception e)
{
var result = new ApiErrorResult(e.Message, e)
{
StatusCode = (int)statusCode,
};
return result;
}
}
It returns an ApiErrorResult with the error message in the reason phrase:
/// <summary>
/// Api error result
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.ObjectResult" />
public class ApiErrorResult : ObjectResult
{
private readonly string _reasonPhrase;
/// <summary>
/// Initializes a new instance of the <see cref="ApiErrorResult"/> class.
/// </summary>
/// <param name="reasonPhrase">The reason phrase.</param>
/// <param name="value">The value.</param>
public ApiErrorResult(string reasonPhrase, object value) : base(value)
{
_reasonPhrase = reasonPhrase;
}
/// <inheritdoc />
public override async Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var reasonPhrase = _reasonPhrase;
reasonPhrase = reasonPhrase.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)[0];
context.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = reasonPhrase;
await base.ExecuteResultAsync(context);
}
}
Thoses filters are set upped in the Startup ConfigureServices method:
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services">A service collection</param>
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(configure =>
{
var filters = configure.Filters;
filters.Add<ApiExceptionFilter>();
filters.Add<ApiCheckModelStateFilter>();
})
.AddJsonOptions(configure =>
{
configure.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
This way I don't need to check if the model is valid in controllers methods:
[HttpPost]
[ValidateAntiForgeryToken]
public Task<IActionResult> UpdateUser(UserProfileViewModel updateUser) => _repo.UpdateUserAsync(updateUser);
I am trying to pass a collection of objects to a method in c#.
Here are the methods.
First one expects a single property to be passed.
/// <summary>
/// Adds an EXIF property to an image.
/// </summary>
/// <param name="inputPath">file path of original image</param>
/// <param name="outputPath">file path of modified image</param>
/// <param name="property"></param>
public static void AddExifData(string inputPath, string outputPath, ExifProperty property)
{
using (Image image = Image.FromFile(inputPath))
{
ExifWriter.AddExifData(image, property);
image.Save(outputPath);
}
}
The second one expects a collection of properties. This is the method I want to pass data too.
/// <summary>
/// Adds a collection of EXIF properties to an image.
/// </summary>
/// <param name="inputPath">file path of original image</param>
/// <param name="outputPath">file path of modified image</param>
/// <param name="properties"></param>
public static void AddExifData(string inputPath, string outputPath, ExifPropertyCollection properties)
{
using (Image image = Image.FromFile(inputPath))
{
ExifWriter.AddExifData(image, properties);
image.Save(outputPath);
}
}
To pass data as a single property I use this code.
// Add folder date to exif tag
ExifProperty folderDate = new ExifProperty();
folderDate.Tag = ExifTag.DateTime;
folderDate.Value = lastPart.ToString();
ExifWriter.AddExifData(imagePath, outputPath, copyright);
Here I only pass one property to the method. How could I send multiple items to the method like this.
// add copyright tag
ExifProperty copyright = new ExifProperty();
copyright.Tag = ExifTag.Copyright;
copyright.Value = String.Format(
"Copyright (c){0} Lorem ipsum dolor sit amet. All rights reserved.",
DateTime.Now.Year);
// Add folder date to exif tag
ExifProperty folderDate = new ExifProperty();
folderDate.Tag = ExifTag.DateTime;
folderDate.Value = lastPart.ToString();
Then pass both these properties?
ExifWriter.AddExifData(imagePath, outputPath, ??????????);
Thanks
You're trying to create a params ExifProperty[] parameter.
public static void AddExifData(string inputPath, string outputPath, params ExifProperty[] properties)
{ ... }
ExifWriter.AddExifData(imagePath, outputPath, copyright, folderDate);
Here is the full class without the new code added.
using System;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;
namespace ExifUtils.Exif.IO
{
/// <summary>
/// Utility class for writing EXIF data
/// </summary>
public static class ExifWriter
{
#region Fields
private static ConstructorInfo ctorPropertyItem = null;
#endregion Fields
#region Write Methods
/// <summary>
/// Adds a collection of EXIF properties to an image.
/// </summary>
/// <param name="inputPath">file path of original image</param>
/// <param name="outputPath">file path of modified image</param>
/// <param name="properties"></param>
public static void AddExifData(string inputPath, string outputPath, ExifPropertyCollection properties)
{
using (Image image = Image.FromFile(inputPath))
{
ExifWriter.AddExifData(image, properties);
image.Save(outputPath);
}
}
/// <summary>
/// Adds an EXIF property to an image.
/// </summary>
/// <param name="inputPath">file path of original image</param>
/// <param name="outputPath">file path of modified image</param>
/// <param name="property"></param>
public static void AddExifData(string inputPath, string outputPath, ExifProperty property)
{
using (Image image = Image.FromFile(inputPath))
{
ExifWriter.AddExifData(image, property);
image.Save(outputPath);
}
}
/// <summary>
/// Adds a collection of EXIF properties to an image.
/// </summary>
/// <param name="image"></param>
/// <param name="properties"></param>
public static void AddExifData(Image image, ExifPropertyCollection properties)
{
if (image == null)
{
throw new NullReferenceException("image was null");
}
if (properties == null || properties.Count < 1)
{
return;
}
foreach (ExifProperty property in properties)
{
ExifWriter.AddExifData(image, property);
}
}
/// <summary>
/// Adds an EXIF property to an image.
/// </summary>
/// <param name="image"></param>
/// <param name="property"></param>
public static void AddExifData(Image image, ExifProperty property)
{
if (image == null)
{
throw new NullReferenceException("image was null");
}
if (property == null)
{
return;
}
PropertyItem propertyItem;
// The .NET interface for GDI+ does not allow instantiation of the
// PropertyItem class. Therefore one must be stolen off the Image
// and repurposed. GDI+ uses PropertyItem by value so there is no
// side effect when changing the values and reassigning to the image.
if (image.PropertyItems == null || image.PropertyItems.Length < 1)
{
propertyItem = ExifWriter.CreatePropertyItem();
}
else
{
propertyItem = image.PropertyItems[0];
}
propertyItem.Id = (int)property.Tag;
propertyItem.Type = (short)property.Type;
Type dataType = ExifDataTypeAttribute.GetDataType(property.Tag);
switch (property.Type)
{
case ExifType.Ascii:
{
propertyItem.Value = Encoding.ASCII.GetBytes(Convert.ToString(property.Value) + '\0');
break;
}
case ExifType.Byte:
{
if (dataType == typeof(UnicodeEncoding))
{
propertyItem.Value = Encoding.Unicode.GetBytes(Convert.ToString(property.Value) + '\0');
}
else
{
goto default;
}
break;
}
default:
{
throw new NotImplementedException(String.Format("Encoding for EXIF property \"{0}\" has not yet been implemented.", property.DisplayName));
}
}
propertyItem.Len = propertyItem.Value.Length;
// This appears to not be necessary
//foreach (int id in image.PropertyIdList)
//{
// if (id == exif.PropertyItem.Id)
// {
// image.RemovePropertyItem(id);
// break;
// }
//}
image.SetPropertyItem(propertyItem);
}
#endregion Write Methods
#region Copy Methods
/// <summary>
/// Copies EXIF data from one image to another
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
public static void CloneExifData(Image source, Image dest)
{
ExifWriter.CloneExifData(source, dest, -1);
}
/// <summary>
/// Copies EXIF data from one image to another
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="maxPropertyBytes">setting to filter properties</param>
public static void CloneExifData(Image source, Image dest, int maxPropertyBytes)
{
bool filter = (maxPropertyBytes > 0);
// preserve EXIF
foreach (PropertyItem prop in source.PropertyItems)
{
if (filter && prop.Len > maxPropertyBytes)
{
// skip large sections
continue;
}
dest.SetPropertyItem(prop);
}
}
#endregion Copy Methods
#region Utility Methods
/// <summary>
/// Uses Reflection to instantiate a PropertyItem
/// </summary>
/// <returns></returns>
internal static PropertyItem CreatePropertyItem()
{
if (ExifWriter.ctorPropertyItem == null)
{
// Must use Reflection to get access to PropertyItem constructor
ExifWriter.ctorPropertyItem = typeof(PropertyItem).GetConstructor(Type.EmptyTypes);
if (ExifWriter.ctorPropertyItem == null)
{
throw new NotSupportedException("Unable to instantiate a System.Drawing.Imaging.PropertyItem");
}
}
return (PropertyItem)ExifWriter.ctorPropertyItem.Invoke(null);
}
#endregion Utility Methods
}
}
You could use the params keyword:
public static void AddExifData(
string inputPath,
string outputPath,
params ExifProperty[] properties)
{
using (Image image = Image.FromFile(inputPath))
{
ExifWriter.AddExifData(image, new ExifPropertyCollection(properties));
image.Save(outputPath);
}
}
And then call:
ExifWriter.AddExifData(imagePath, outputPath, copyright, folderDate);