A previous post showed how to import the iTunes library into a Spotfire data table. This post explains how to add an obvious end user feature: When a row is selected in Spotfire, the corresponding song is played in iTunes.
To do that we first create a Tool that will show up in the menu to start listening to markings, we do not actually perform any work in our tool but instead create a custom node that will perform all the work.
public sealed class ITunesTool : CustomTool<Document>
{
public ITunesTool() : base("Connect marking with iTunes")
{
// Empty
}
protected override void ExecuteCore(Document context)
{
context.CustomNodes.AddNewIfNeeded<ITunesMarkingNode>();
}
}
Then we need to create the ITunesMarking node which is a custom node that is a document node so we need to follow the standard design patterns for document nodes. In our case we want to have references to both a marking and a data table so we add UndoableCrossReferenceProperty fields for both these types.
[PersistenceVersion(1,0)]
[Serializable]
public sealed class ITunesMarkingNode : CustomNode
{
private readonly UndoableCrossReferenceProperty<DataMarkingSelection> marking;
private readonly UndoableCrossReferenceProperty<DataTable> table;
}
Now we can use the new Visual Studio® Macros that were added in 2.2 to generate the boilerplate code for the class.
This will give us the following resulting class:
[PersistenceVersion(1, 0)]
[Serializable]
public sealed class ITunesMarkingNode : CustomNode
{
private readonly UndoableCrossReferenceProperty<DataMarkingSelection> marking;
private readonly UndoableCrossReferenceProperty<DataTable> table;
#region Classes for property names
/// <summary>
/// Contains property name constants for the public properties of <see cref="ITunesMarkingNode"/>.
/// </summary>
public new abstract class PropertyNames : CustomNode.PropertyNames
{
/// <summary>
/// The name of the property Marking.
/// </summary>
public static readonly PropertyName Marking = CreatePropertyName("Marking");
/// <summary>
/// The name of the property Table.
/// </summary>
public static readonly PropertyName Table = CreatePropertyName("Table");
}
#endregion // Classes for property names
#region Public properties
/// <summary>
/// Gets or sets Marking.
/// </summary>
public DataMarkingSelection Marking
{
get { return this.marking.Value; }
set { this.marking.Value = value; }
}
/// <summary>
/// Gets or sets Table.
/// </summary>
public DataTable Table
{
get { return this.table.Value; }
set { this.table.Value = value; }
}
#endregion // Public properties
#region Construction
/// <summary>
/// Initializes a new instance of the <see cref="T:ITunesMarkingNode"/> class./// </summary>
public ITunesMarkingNode()
{
CreateProperty(PropertyNames.Marking, out this.marking, default(DataMarkingSelection));
CreateProperty(PropertyNames.Table, out this.table, default(DataTable));
}
#endregion // Construction
#region ISerializable Members
/// <summary>Implements ISerializable.</summary>
protected ITunesMarkingNode(SerializationInfo info, StreamingContext context)
: base(info, context)
{
DeserializeProperty<DataMarkingSelection>(info, context, PropertyNames.Marking, out this.marking);
DeserializeProperty<DataTable>(info, context, PropertyNames.Table, out this.table);
}
/// <summary>Implements ISerializable.</summary>
protected override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
SerializeProperty<DataMarkingSelection>(info, context, this.marking);
SerializeProperty<DataTable>(info, context, this.table);
}
#endregion // ISerializable Members
}
The marking and table properties will be empty after creating the object so we override the OnConfigure method to set some good default values for the marking and the table. The OnConfigure method is called when a newly created node is added to a document.
protected override void OnConfigure()
{
base.OnConfigure();
// Get the document that this node is part of.
Document document = this.Context.GetAncestor<Document>();
// Get the marking used in the currently active plot, may be null.
DataMarkingSelection marking = document.ActiveMarkingSelectionReference;
if(marking == null)
{
// Use the default marking if no plot was selected.
marking = document.Data.Markings.DefaultMarkingReference;
}
// Select the chosen marking.
this.Marking = marking;
// Now we need to choose a table to use and we require that certain columns
// exists in the table.
foreach (DataTable dataTable in document.Data.Tables)
{
if (dataTable.Columns.Contains("SourceID") &&
dataTable.Columns.Contains("PlaylistID") &&
dataTable.Columns.Contains("TrackID") &&
dataTable.Columns.Contains("DataBaseID"))
{
// Has all our columns, let's use that.
this.Table = dataTable;
break;
}
}
}
Now that we have all the state set up correctly we need to setup the event handlers so that we can execute code to talk to iTunes whenever the selected marking changes. In our case we need to use internal event handlers so we override the DeclareInternalEventHandlers method. We have to use a mutable property trigger since we are using a cross reference to the marking node and we trigger when the selection changes in the marking. When the marking changes our MarkingChanged method will be called.
protected override void DeclareInternalEventHandlers(InternalEventManager eventManager)
{
base.DeclareInternalEventHandlers(eventManager);
eventManager.AddEventHandler(
MarkingChanged,
Trigger.CreateMutablePropertyTrigger<DataMarkingSelection>(
this,
ITunesMarkingNode.PropertyNames.Marking,
DataMarkingSelection.PropertyNames.Selection));
}
private void MarkingChanged(DocumentNode node, PropertyName propertyName)
{
}
Now we need to add some code to call iTunes in the MarkingChanged method.
private void MarkingChanged(DocumentNode node, PropertyName propertyName)
{
if (this.Marking == null || this.Table == null)
{
return;
}
// Get the currently marked rows.
IndexSet markedRows = this.Marking.GetSelection(this.Table).AsIndexSet();
if (markedRows.Count == 0)
{
// No rows marked, return.
return;
}
// Pick the first marked row.
int markedRow = markedRows.First;
// Retrieve all the columns that we need from the table.
DataColumn sourceId;
if (!Table.Columns.TryGetValue("SourceID", out sourceId))
{
return;
}
DataColumn playlistID;
if (!Table.Columns.TryGetValue("PlaylistID", out playlistID))
{
return;
}
DataColumn trackID;
if (!Table.Columns.TryGetValue("TrackID", out trackID))
{
return;
}
DataColumn dataBaseID;
if (!Table.Columns.TryGetValue("DataBaseID", out dataBaseID))
{
return;
}
// Get the values from the columns for the marked row.
int sid = (int)sourceId.RowValues.GetValue(markedRow).Value;
int pid = (int)playlistID.RowValues.GetValue(markedRow).Value;
int tid = (int)trackID.RowValues.GetValue(markedRow).Value;
int did = (int)dataBaseID.RowValues.GetValue(markedRow).Value;
ITunesController.GetInstance().PlaySong(sid, pid, tid, did);
}
And then implement the PlaySong method in the ITunesController class that we created in the previous article.
internal void PlaySong(int sid, int pid, int tid, int did)
{
Invoke(
delegate
{
iTunesLib.IITTrack track =
this.iTunesApplication.GetITObjectByID(
sid, pid, tid, did) as iTunesLib.IITTrack;
if (track != null)
{
track.Play();
}
});
}
Now we only need to add the tool to the AddIn and we are done.
public class ITunesAddIn : AddIn
{
protected override void RegisterDataSources(
AddIn.DataSourceRegistrar registrar)
{
base.RegisterDataSources(registrar);
registrar.Register<ITunesDataSource>(
ITunesTypeIdentifiers.ITunesDataSourceTypeIdentifier);
}
protected override void RegisterTools(AddIn.ToolRegistrar registrar)
{
base.RegisterTools(registrar);
registrar.Register(new ITunesTool());
}
}
Now we can use the data source written in the previous article to get data into Spotfire and then execute the tool written in this article to start playing the songs selected using marking.