Contents
This article has been in the making a while, and has sort of become a labour of love. When I first started, the sole purpose of this article was to gain a deeper understanding of some of LINQ internals; I just wanted to gain a better understanding of IQueryProvider
and its role in the scheme of things. I needed an arena in which to perform this self study, so what I decided to do was construct a workable/searchable MP3 player. The MP3 player would work of MP3 ID3 tag metadata which would be queryable using IQueryProvider
, and would allow the user's MP3s to be persisted to a database (SQL Server).
In essence, this is what this article is all about.
Before you proceed to read the rest of this article, I would just ask you to read the Voting Games section at the bottom of this article.
Before you try and run Sonic at home/work, you will need to install the SQL Server database and change the connection string in the Settings file, and also create your own musicLocationPath
(see App.Config).
This article uses some items from .NET 3.5 SP1, so that is a pre-requisite. Sorry.
Essentially, Sonic (the codename, get the music link) looks like this. It is made up of a number of different constituent Views, and each View is driven by a dedicated ViewModel, which allows the View to bind seamlessly to the ViewModel data. What this also means is that there is very little code-behind in each of the Views, as all the heavy lifting is done by the ViewModel associated with a given View.
We will study each of the Views/ViewModels in more detail later in the article, but for now, here is a brief run down:
MainWindow
: Hosts a MediaView
View and a top control banner, and also has some common buttons for minimise/maximise/close.MediaView
View: Hosts an ItemsControl
which has a number of AlbumView
Views which are driven from their own ViewModels. It also hosts another ItemsControl
which has a number of MP3FileView
items (again, driven by their own ViewModels), and lastly, it hosts a 3D album art view, which is called AlbumView3D
.
We will get into how all this hangs together a bit later on, but for now, just be aware that there are a number of Views that form the whole app, and each View has a ViewModel to deal with its logic.
I have tried to write this application in a logical manner, and I hope it works as most people would expect it to work. So without further ado, let me delve into how I intended Sonic to work.
When Sonic is first run, it will examine the Settings associated with the Sonic application and look at the "ReReadAllFiles
" setting which is initially set to true
. If it finds that this flag is set to true
, Sonic will scan all the available and valid music (basically only MP3s) that is available using the locations that you specify within the App.Config using the custom Sonic configuration section.
When valid MP3s are found, the ID3 metadata associated with each valid file is stored within a SQL Server table (see the top of this article for the scripts to create that database).
Important note
Once all of the musicLocationPath
(s) have been scanned (as configured within the App.Config), the Sonic setting "ReReadAllFiles
" will be set to false
, such that future runs of Sonic will not cause an entire scan process to occur. So you will need to configure the App.Config to point to your music paths before running Sonic the first time.
So assuming you have successfully scanned in all your music, you will be able to search it using the associated ID3 available metadata. To facilitate this, I have allowed searches to be performed on the following metadata:
- Artist start letter
- Genre
- Song name
- Artist name
When a search yields results, an ItemsControl
within the MediaView
will be populated to show the matching albums (AlbumView
). From where you can click one of the albums (AlbumView
) and have it show a list of the tracks (a number of MP3FileView
s) in that album; the album art will be shown in a larger view using a 3D type view (AlbumView3D
).
When the list of tracks (a number of MP3FileView
s) is shown, you will be able to click on a track and it will play using a WPF MediaPlayer
element.
In essence, that is it. Obviously, there is slightly more to it than that; otherwise, it would not have taken me so long to write.
OK, now that you know what I was aiming towards, you should know that there is some user configuration required in order to make Sonic work correctly. In order to do this, I have created a custom configuration section which allows you (the user) to specify your own MP3 directory locations.
For me, I store all my music in one or two top level folders, so my personal Sonic configuration section only has one or two entries. Yours may be different.
Anyway that is by the by, let's have a look at the App.Config part that deals with the custom Sonic configuration section; well, for me, it is configured like this:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="MusicLocationLookup"
type="Sonic.MusicLocationLookupConfigSection, Sonic" />
</configSections>
<MusicLocationLookup>
<MusicRepository>
<add musicLocationPath="E:\MP3's" />
</MusicRepository>
</MusicLocationLookup>
So that is all well and good; we can put Sonic related data into its own Config section, but how does that all work? Well, it works by using two dedicated configuration classes. Let's see each of them, shall we?
MusicLocationLookupConfigSection
: which is the actual custom Config section class:
public class MusicLocationLookupConfigSection : ConfigurationSection
{
#region Public Properties
[ConfigurationProperty("MusicRepository")]
public MusicLocationElementCollection MusicLocations
{
get { return
((MusicLocationElementCollection)
(base["MusicRepository"])); }
}
#endregion
}
MusicLocationElementCollection
: which allows for a collection of MusicLocationElement
(s) to be added to the App.Config:
[ConfigurationCollection(typeof(MusicLocationElement))]
public class MusicLocationElementCollection
: ConfigurationElementCollection
{
#region Overrides
protected override ConfigurationElement
CreateNewElement()
{
return new MusicLocationElement();
}
protected override object GetElementKey(
ConfigurationElement element)
{
return ((MusicLocationElement)(element)).musicPath;
}
#endregion
#region Public Properties
public MusicLocationElement this[int idx]
{
get
{
return (MusicLocationElement)BaseGet(idx);
}
}
#endregion
}
MusicLocationElement
: which is an actual directory name of where your music files are stored:
public class MusicLocationElement
: ConfigurationElement
{
#region Public Properties
[ConfigurationProperty("musicLocationPath",
DefaultValue = "", IsKey = true,
IsRequired = true)]
public string musicPath
{
get { return ((string)(base["musicLocationPath"])); }
set { base["musicLocationPath"] = value; }
}
#endregion
}
I really like the fact that Microsoft opened up Configuration for extension, it makes it quite nice to work with.
As previously stated, there are a number of settings that Sonic uses to do various things. These settings are as follows:
ReReadAllFiles
: Which when set to true will force Sonic to re-read all the music as scanned by using the MusicLocationElement
(s) that you specified within the App.Config. This setting will be set to false by Sonic after its initial scan. Settings files are strange, and a local copy is actually stored when the Save()
method is called on the setting based class. Which is something that Sonic does. Basically, after an initial scan, the "ReReadAllFiles
" setting is set to false and the settings are saved, which means even if you set the "ReReadAllFiles
" setting back to true in the App.Config, Sonic will use the previously saved version. So if you forget to configure valid MusicLocationElement
(s) or would like Sonic to re-scan all your music after the initial run, you will need to locate and delete the saved settings file. For me, this is within a directory named C:\Users\sacha\AppData\Local\Sonic\ Sonic.vshost.exe_Url_hgefzieuxosag5swhmgx4xzo5rtoslkn\0.0.0.0 (yours will be different, but will be roughly in the same place in your profile).AttemptToGainWebAlbumArt
: Instructs Sonic that you would like to search Google (yes, that's right, Sonic is capable of doing Google searches) for album art images, instead of looking locally for hard drive stored album art. It is important to note that there is a large impact on doing this. For example, if Sonic is returning 20 albums worth of MP3s for a query, these would load almost instantly when not trying to obtain web based artwork, but take about a minute when trying to grab album art images from Google. It takes a while to do. But I think the effect of having all the correct artwork is worth the wait. But if this delay annoys you, simply toggle this flag to false, and Sonic will try and use local artwork, and if there is none available, a default image will be used for the album art.
Whilst constructing Sonic, I had to create a number of custom controls that did various things, I will now describe these.
CircularProgressBar
I wanted a marquee style progress bar that was like the web based circular ones that have become popular these days. So I had a look at this and came up with a simple idea; just arrange some ellipses in XAML and do a never ending rotate on the entire control. It's quite effective, and looks like this:
The only thing worth mentioning here is that I changed the default animation frame rate by overriding the metadata associated with StoryBoard
types within the constructor of the CircularProgressBar
control.
static CircularProgressBar()
{
Timeline.DesiredFrameRateProperty.OverrideMetadata(
typeof(Timeline),
new FrameworkPropertyMetadata { DefaultValue = 30 });
}
And here is the XAML. Easy, right?
<UserControl x:Class="Sonic.CircularProgressBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="120" Width="120" Background="Transparent">
<Grid x:Name="LayoutRoot" Background="Transparent"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.RenderTransform>
<ScaleTransform x:Name="SpinnerScale"
ScaleX="1.0" ScaleY="1.0" />
</Grid.RenderTransform>
<Canvas RenderTransformOrigin="0.5,0.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="120" Height="120" >
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="20.1696"
Canvas.Top="9.76358" Stretch="Fill"
Fill="Red" Opacity="1.0"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="2.86816"
Canvas.Top="29.9581" Stretch="Fill"
Fill="Orange" Opacity="0.9"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="5.03758e-006"
Canvas.Top="57.9341" Stretch="Fill"
Fill="Orange" Opacity="0.8"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="12.1203"
Canvas.Top="83.3163" Stretch="Fill"
Fill="Orange" Opacity="0.7"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="36.5459"
Canvas.Top="98.138" Stretch="Fill"
Fill="Orange" Opacity="0.6"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="64.6723"
Canvas.Top="96.8411" Stretch="Fill"
Fill="Orange" Opacity="0.5"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="87.6176"
Canvas.Top="81.2783" Stretch="Fill"
Fill="Orange" Opacity="0.4"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="98.165"
Canvas.Top="54.414" Stretch="Fill"
Fill="Orange" Opacity="0.3"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="92.9838"
Canvas.Top="26.9938" Stretch="Fill"
Fill="Orange" Opacity="0.2"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="47.2783"
Canvas.Top="0.5" Stretch="Fill"
Fill="Orange" Opacity="0.1"/>
<Canvas.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" Angle="0" />
</Canvas.RenderTransform>
<Canvas.Triggers>
<EventTrigger RoutedEvent="ContentControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="SpinnerRotate"
Storyboard.TargetProperty="(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:01"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
</Grid>
</UserControl>
FrictionScrollViewer
I wanted the list of albums that matched a query to be contained in a special friction enabled ScrollViewer
; to that end, I created this class which did the trick:
public class FrictionScrollViewer : ScrollViewer
{
#region Data
private DispatcherTimer animationTimer = new DispatcherTimer();
private Point previousPoint;
private Point scrollStartOffset;
private Point scrollStartPoint;
private Point scrollTarget;
private Vector velocity;
private Point autoScrollTarget;
private bool shouldAutoScroll = false;
#endregion
#region Ctor
static FrictionScrollViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(FrictionScrollViewer),
new FrameworkPropertyMetadata(typeof(FrictionScrollViewer)));
}
public FrictionScrollViewer()
{
Friction = 0.95;
animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
animationTimer.Tick += HandleWorldTimerTick;
animationTimer.Start();
}
#endregion
#region DPs
public double Friction
{
get { return (double)GetValue(FrictionProperty); }
set { SetValue(FrictionProperty, value); }
}
public static readonly DependencyProperty FrictionProperty =
DependencyProperty.Register("Friction", typeof(double),
typeof(FrictionScrollViewer), new UIPropertyMetadata(0.0));
#endregion
#region overrides
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (IsMouseOver)
{
shouldAutoScroll = false;
scrollStartPoint = e.GetPosition(this);
scrollStartOffset.X = HorizontalOffset;
scrollStartOffset.Y = VerticalOffset;
Cursor = (ExtentWidth > ViewportWidth) ||
(ExtentHeight > ViewportHeight) ?
Cursors.ScrollAll : Cursors.Arrow;
CaptureMouse();
}
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (IsMouseCaptured)
{
shouldAutoScroll = false;
Point currentPoint = e.GetPosition(this);
Point delta = new Point(scrollStartPoint.X -
currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
scrollTarget.X = scrollStartOffset.X + delta.X;
scrollTarget.Y = scrollStartOffset.Y + delta.Y;
ScrollToHorizontalOffset(scrollTarget.X);
ScrollToVerticalOffset(scrollTarget.Y);
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
if (IsMouseCaptured)
{
Cursor = Cursors.Arrow;
ReleaseMouseCapture();
}
base.OnMouseUp(e);
}
#endregion
#region Animation timer Tick
private void HandleWorldTimerTick(object sender, EventArgs e)
{
if (IsMouseCaptured)
{
Point currentPoint = Mouse.GetPosition(this);
velocity = previousPoint - currentPoint;
previousPoint = currentPoint;
}
else
{
if (shouldAutoScroll)
{
Point currentScroll = new Point(ScrollInfo.HorizontalOffset +
ScrollInfo.ViewportWidth / 2.0,
ScrollInfo.VerticalOffset + ScrollInfo.ViewportHeight / 2.0);
Vector offset = autoScrollTarget - currentScroll;
shouldAutoScroll = offset.Length > 2.0;
ScrollToHorizontalOffset(HorizontalOffset + offset.X / 10.0);
ScrollToVerticalOffset(VerticalOffset + offset.Y / 10.0);
}
else
{
if (velocity.Length > 1)
{
ScrollToHorizontalOffset(scrollTarget.X);
ScrollToVerticalOffset(scrollTarget.Y);
scrollTarget.X += velocity.X;
scrollTarget.Y += velocity.Y;
velocity *= Friction;
System.Diagnostics.Debug.WriteLine("Scroll @ " +
ScrollInfo.HorizontalOffset + ", " +
ScrollInfo.VerticalOffset);
}
}
InvalidateScrollInfo();
InvalidateVisual();
}
}
#endregion
#region Public Methods/Properties
public Point AutoScrollTarget
{
set
{
autoScrollTarget = value;
shouldAutoScroll = true;
}
}
public void ScrollToCenterTarget(Point target)
{
ScrollToHorizontalOffset(target.X - ScrollInfo.ViewportWidth / 2.0);
ScrollToVerticalOffset(target.Y - ScrollInfo.ViewportHeight / 2.0);
}
#endregion
}
You can see this in use within this area of Sonic; simple place your mouse down and drag left or right quickly and let go, and to stop it, click the mouse again (it only drags when not directly over an album):
FancyButton
The only other control I have is a fancy (very fancy button, which I stole from the Blend samples) button. As I stole this, I will not go into the code, but it looks like this, and it has lots of cool StoryBoard
animations to make it work. I like it anyway.
I do not really like the way the standard WPF Window
looks, so I decided to tackle re-styling it. Luckily, Microsoft has made this pretty easy to do with some standard XAML control templating techniques. There is actually a very good MSDN link that shows the default control templates that the standard controls (of which, Window
is one) use.
So armed with this knowledge, it was simply a case of restyling the window the way I wanted. So instead of this Window
style:
I would get the following, which is pretty much a blank resizable (with Grip) window. Which allows me to place other controls on for things like minimize/maximize/close.
You do need to specify a few Window
level properties to make this happen. Here is an example:
<Window x:Class="Sonic.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Sonic : Music Library"
Background="{x:Null}"
Topmost="False"
WindowStartupLocation="CenterScreen"
WindowState="Normal"
MinHeight="620"
MinWidth="950"
WindowStyle="None"
Template="{StaticResource WindowTemplateKey}"
ResizeMode="CanResizeWithGrip" AllowsTransparency="True">
<Grid Background="WhiteSmoke">
</Grid>
</Window>
Where the actual Window
control template is defined as follows. In the attached demo app, all styles are declared within the ResourceDictionary
AppStyles.xaml.
<ControlTemplate x:Key="WindowTemplateKey" TargetType="{x:Type Window}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<AdornerDecorator>
<ContentPresenter/>
</AdornerDecorator>
<ResizeGrip Visibility="Collapsed"
HorizontalAlignment="Right" x:Name="WindowResizeGrip"
Style="{DynamicResource ResizeGripStyle1}"
VerticalAlignment="Bottom" IsTabStop="false"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ResizeMode" Value="CanResizeWithGrip"/>
<Condition Property="WindowState" Value="Normal"/>
</MultiTrigger.Conditions>
<Setter Property="Visibility"
TargetName="WindowResizeGrip" Value="Visible"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Sonic relies heavily on ID3 tag metadata that may or may not be available within scanned files. Now there are two major versions of the ID3 specification ID3v1, which is fairly simple and actually looks like this:
Which is pretty easy to byte strip; in fact, I have done this myself on a previous project,;however, ID3v2 is a different beast altogether. And to be frank, I couldn't be bothered doing this, so I had a hunt about and found an excellent free ID3 library for .NET which is called UltraID3Lib, which reads both ID3v1 and ID3v2 tags. It is very easy to use, and is obviously included in Sonic.
Here is what it looks like to read an ID3 tag for a given file, where I am returning a specialised MP3
object which has extra functions that I needed within Sonic:
public static MP3 ProcessSingleMP3File(String fileName)
{
MP3 mp3File = null;
Boolean hasTag = false;
String album = String.Empty;
String artist = String.Empty;
String genreName = String.Empty;
String title = String.Empty;
UltraID3 readMP3File = new UltraID3();
readMP3File.Read(fileName);
if (readMP3File.ID3v2Tag.ExistsInFile)
{
hasTag = true;
album = readMP3File.ID3v2Tag.Album;
artist = readMP3File.ID3v2Tag.Artist;
genreName = readMP3File.ID3v2Tag.Genre;
title = readMP3File.ID3v2Tag.Title;
}
if (readMP3File.ID3v1Tag.ExistsInFile && !hasTag)
{
hasTag = true;
album = readMP3File.ID3v1Tag.Album ?? "Uknown";
artist = readMP3File.ID3v1Tag.Artist ?? "Uknown";
genreName = readMP3File.ID3v1Tag.GenreName ?? "Uknown";
title = readMP3File.ID3v1Tag.Title ?? "Uknown";
}
if (hasTag)
{
mp3File = new MP3();
mp3File.FileName = fileName;
mp3File.Album = album;
mp3File.Artist = artist;
mp3File.GenreName = genreName;
mp3File.Title = title;
}
return mp3File;
}
As I stated at the start of this article, one of the main reasons I wanted to write this article was to get a bit more familiar with LINQ and IQueryProvider
. I should point out right now that Sonic does not and will never create an entire IQueryProvider
implementation, that is an insane amount of work. Basically, what Sonic does is cheat. As Sonic is using SQL Server behind the scenes (LINQ to SQL is an implementation of IQueryProvider
, wouldn't you know?), I merely intercept the original query which is an Expression
tree, which is what IQueryProvider
implementations must work with, and delegate that off to some logic which does the work using my SQL Server DataContext
which deals with parsing the Expression
tree into a SQL command.
You may ask yourself, why the hell I did that, and you would be right to ask that. Well, to be honest, you would bypass the IQueryProvider
implementation within Sonic altogether and go straight to the LINQ to SQL database DataContext
, but where is the fun in that? We want to develop a better understanding, don't we? Basically, that is why I did this extra step.
Trying to write an entire IQueryProvider
implementation is not for the faint hearted. If you want to know more about this subject, you can read more at Matt Warren's site: LINQ: Building an IQueryable provider series.
OK, so now, all that is said, how does it all work in terms of Sonic?
Well, quite simply, it works like this:
- The
MediaViewModel
has a number of pre-built parameter driven Expression
s that are used to feed into my own MP3Provider
, which implements QueryProvider
(which is a base class that I stole from Matt Warren's site). - What the
QueryProvider
does is use the incoming Expression
tree, by overriding the public override object Execute(Expression expression)
method of QueryProvider
. Basically, when working with IQueryProvider
, we must work with Expression
trees, and not delegates (Func<T,TResult>
); the reason being that IQueryProvider
is intended to work with things like SQL which use other storage mechanisms/grammar and would not understand what to do with a delegate (Func<T,TResult>
), so Expression
trees must be used. Typically, the entire Expression
tree would be examined using the Visitor pattern, which allows the correct query to be dynamically built for the visited Expression
tree. So you can see, by having a specific IQueryProvider
implementation, you could use the same Expression
tree with numerous IQueryProvider
implementation(s). Each IQueryProvider
implementation would essentially form its own specific query based on the correct grammar/syntax for the object that it is providing values for. In the case of LINQ to SQL (which is an IQueryProvider
implementation), this would be to create a SQL query.
- The
Expression
tree that is passed to the public override object Execute(Expression expression)
method of QueryProvider
is then used to pass to a helper class that does the real work of using the Expression
tree and compiling it into a delegate (Func<T,TResult>
), which can be used against the Sonic database DataContext
(which as I stated is a IQueryProvider
implementation).
This all sounds fairly mental, but perhaps it will become clearer when we work though an example.
OK, so let's start with the source of the query, which is via the search button on the MainWindow
, which when pressed will instruct the embedded MediaView
(which uses its ViewModel to do the work) to perform a certain type of query. It does this by using Commands, but we will cover that later. For now, let's concentrate on understanding this query mechanism.
We have an Expression
within MediaViewViewModel
which looks like this, which is an expression that uses a Func<MP3,Boolean>
as a selector (or predicate) to filter a collection of MP3
types:
private Expression<Func<MP3, Boolean>> queryExpression = null;
And when we do some sort of search, such as try and search via "Artist Letter":
This will set the current Expression
within the MediaViewViewModel
to be something like:
public String CurrentArtistLetter
{
get { return currentArtistLetter; }
set
{
currentArtistLetter = value;
NotifyPropertyChanged("CurrentArtistLetter");
QueryToPerform = OverallQueryType.ByArtistLetter;
queryExpression =
mp3 => mp3.Artist.ToLower().
StartsWith(currentArtistLetter.ToLower());
}
}
We now have an Expression
within the MediaViewViewModel
that can be used to search for MP3
types. Next, we need to look at how this query is used against the Sonic QueryProvider
implementation.
When the query is run within the MediaViewViewModel
, the RunQuery()
method is run, which looks like this:
private void RunQuery(Expression<Func<MP3, Boolean>> expr)
{
try
{
ThreadPool.QueueUserWorkItem(x =>
{
IsBusy = true;
MP3s MP3 = new MP3s();
IQueryable<MP3> query = MP3.Files.Where<MP3>(expr);
var mp3sMatched = query.ToList();
var albumsOfMP3s =
from mp3 in mp3sMatched
group mp3 by mp3.Album;
ObservableCollection<AlbumOfMP3ViewModel> albums =
new ObservableCollection<AlbumOfMP3ViewModel>();
double animationOffset = 100;
double currentAnimationTime = 0;
foreach (var album in albumsOfMP3s)
{
List<MP3> albumFiles = album.ToList();
AlbumOfMP3ViewModel albumOfMP3s = new AlbumOfMP3ViewModel
{
Album = albumFiles.First().Album,
Artist = albumFiles.First().Artist,
Files = albumFiles
};
albumOfMP3s.ObtainImageForAlbum();
albumOfMP3s.AnimationDelayMs =
currentAnimationTime += animationOffset;
albums.Add(albumOfMP3s);
}
AlbumsReturned = albums;
IsBusy = false;
});
}
catch (Exception ex)
{
Console.WriteLine("Ooops, its busted " + ex.Message);
}
finally
{
IsBusy = false;
}
}
The most important part of this, by far, is these two lines:
MP3s MP3 = new MP3s();
IQueryable<MP3> query = MP3.Files.Where<MP3>(expr);
What is happening there is a new MP3
object which contains an internal Sonic QueryProvider
implementation.
public class MP3s
{
private IQueryProvider provider;
public IQueryable<MP3> Files;
public MP3s()
{
this.provider = new MP3Provider();
this.Files = new Query<MP3>(this.provider);
}
public IQueryProvider Provider
{
get { return this.provider; }
}
}
Now that we can see what that is all about, we can turn our attention to the Sonic QueryProvider
implementation.
This works as follows:
public class MP3Provider : QueryProvider
{
public override object Execute(Expression expression)
{
MethodCallExpression mex = expression as MethodCallExpression;
Expression<Func<MP3,Boolean>> lambdaExpression =
(Expression<Func<MP3, Boolean>>)
(mex.Arguments[1] as UnaryExpression).Operand;
Func<MP3, Boolean> filter = lambdaExpression.Compile();
return XMLAndSQLQueryOperations.GetMatchingMP3Files(filter);
}
}
Where this inherits from (from Matt Warren's site) the QueryProvider
base class.
public abstract class QueryProvider : IQueryProvider
{
#region Ctor
protected QueryProvider()
{
}
#endregion
#region IQueryProvider Members
IQueryable<T> IQueryProvider.CreateQuery<T>(Expression expression)
{
return new Query<T>(this, expression);
}
public IQueryable CreateQuery(Expression expression)
{
throw new NotImplementedException();
}
public T Execute<T>(Expression expression)
{
return (T)this.Execute(expression);
}
public abstract object Execute(Expression expression);
#endregion
}
This may look completely nuts, but all that is happening is the original Expression
which was something like:
queryExpression =
mp3 => mp3.Artist.ToLower().
StartsWith(currentArtistLetter.ToLower());
is compiled into a Func<MP3,Boolean>
selector (predicate, if you prefer) which can be used against a IEnumerable<MP3>
or Query<MP3>
.
If we examine the XMLAndSQLQueryOperations.GetMatchingMP3Files(filter)
method which uses this Func<MP3,Boolean>
predicate, it will hopefully become clearer.
public static IQueryable<MP3> GetMatchingMP3Files(Func<MP3, Boolean> filter)
{
SQLMP3sDataContext datacontext = new SQLMP3sDataContext();
return datacontext.MP3s.Where(filter).AsQueryable();
}
See, behind the scenes, it is just using standard LINQ to SQL IEnumerable<MP3>
stuff which works with the standard LINQ to SQL DataContext
; the only thing it does is cast it to ensure the results are using IQueryable<MP3>
. Basically, if a type supports IQueryable<T>
, it will use IQueryable<T>
, and if it doesn't support being queried, it will use IEnumerable<T>
, which uses delegates instead of Expression
trees.
Recall that we used a line like this:
MP3s MP3 = new MP3s();
IQueryable<MP3> query = MP3.Files.Where<MP3>(expr);
So we are returning IQueryable<MP3>
, but how's that LINQ to SQL doesn't do that out of the box? There is one final step which is to make the types you want to query, queryable. Here is how (again, borrowed from Matt Warren's site):
public class Query<T> : IQueryable<T>, IQueryable,
IEnumerable<T>, IEnumerable,
IOrderedQueryable<T>, IOrderedQueryable
{
IQueryProvider provider;
Expression expression;
public Query(IQueryProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException("provider");
}
this.provider = provider;
this.expression = Expression.Constant(this);
}
public Query(QueryProvider provider, Expression expression)
{
if (provider == null)
{
throw new ArgumentNullException("provider");
}
if (expression == null)
{
throw new ArgumentNullException("expression");
}
if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type))
{
throw new ArgumentOutOfRangeException("expression");
}
this.provider = provider;
this.expression = expression;
}
Expression IQueryable.Expression
{
get { return this.expression; }
}
Type IQueryable.ElementType
{
get { return typeof(T); }
}
IQueryProvider IQueryable.Provider
{
get { return this.provider; }
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)
this.provider.Execute(this.expression))
.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)
this.provider.Execute(this.expression))
.GetEnumerator();
}
public override string ToString()
{
if (this.expression.NodeType == ExpressionType.Constant &&
((ConstantExpression)this.expression).Value == this)
{
return "Query(" + typeof(T) + ")";
}
else
{
return this.expression.ToString();
}
}
}
And breath, you are there. I know this looks insane (well, for me, it is anyhow), but remember you could bypass all this, and simply declare the searches as a Func<MP3,Boolean>
predicate instead of Expression trees, and skip all this and use the Func<MP3,Boolean>
predicates against LINQ to SQL directly, and work with IEnumerable<MP3>
instead.
I just wanted to delve a little deeper is all.
If you are trying to get to grips with WPF development, you will want to use the MVVM pattern.
There are a number of great sources regarding this pattern; here are some links:
Sonic actually uses a number of different Views, each of which has its own ViewModel.
Recall this image:
Each of these Views has a dedicated ViewModel. The basic idea is that the View is able to delegate its actions to a ViewModel, and that it is able to bind to its ViewModel using WPF databinding techniques.
The delegation of commands basically means not doing stuff in code-behind, but rather get the ViewModel to do the work and have it update its properties which the Views sees and can adjust its visual representation to reflect.
In order to make this work, there are a number of key things.
Commands
WPF comes with RoutedCommand(s) which allow ViewModels to hold Commands (ICommand
implementations) which can hold methods that are run when the command is run from the item that is using the command. The standard RoutedCommand(s) idea is cool, but requires a little bit of XAML/code-behind work in the View, and some bright people have put some time into coming up with alternatives. One such person was Marlon Grech, who wrote a nice delegate style command, which means you can do away with having to do stuff in the View and do it in the ViewModel.
Here is an example:
The ICommand
implementation looks like:
public class SimpleCommand : ICommand
{
public Predicate<object> CanExecuteDelegate { get; set; }
public Action<object> ExecuteDelegate { get; set; }
#region ICommand Members
public bool CanExecute(object parameter)
{
if (CanExecuteDelegate != null)
return CanExecuteDelegate(parameter);
return true;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
if (ExecuteDelegate != null)
ExecuteDelegate(parameter);
}
#endregion
}
Which allows us to simply have ICommand
properties on our ViewModel which the View can bind to.
public class MediaViewViewModel : ViewModelBase
{
private ICommand runQueryCommand = null;
public MediaViewViewModel()
{
runQueryCommand = new SimpleCommand
{
CanExecuteDelegate = x => !IsBusy && queryExpression != null,
ExecuteDelegate = x => RunQuery(queryExpression)
};
private void RunQuery(Expression<Func<MP3, Boolean>> expr)
{
....
....
....
}
}
}
Which allows the View to simply use this command like this:
<local:FancyButton ButtonToolTip="Search For Music Using This Query"
ButtonCommand="{Binding Path=MediaViewVM.RunQueryCommand}"/>
Can you see from this, we can wire up the View to the ViewModel logic. No problems.
INotifyPropertyChanged
The other holy grail is INPC, which simply allows for change notification to be seen by Bindings.
I typically make a base class that I inherit from that deals with this. Here is an example:
public abstract class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
OK, so now that we have the basics covered, let's look at a few of these ViewModels, shall we? I will not cover all of them, but I would like to spend a little bit of time talking about maybe one or two of them.
One thing that I have personally noticed is that there are simply no examples that deal with, well, a more complex problem than just showing a list of Xs and updating a certain X. I understand why this is the case; it is because people understand that problem domain. However, real life is not that simple, so I decided to make Sonic involve things like animations etc.
So without further ado, let's consider a ViewModel or two.
AlbumOfMP3ViewModel
The MediaViewViewModel
actually has a property on it which is an ObservableCollection<AlbumOfMP3ViewModel> AlbumsReturned
that is used to represent the grouped albums of MP3
s that matches a particular search.
ObservableCollection<AlbumOfMP3ViewModel> albums =
new ObservableCollection<AlbumOfMP3ViewModel>();
double animationOffset = 100;
double currentAnimationTime = 0;
foreach (var album in albumsOfMP3s)
{
List<MP3> albumFiles = album.ToList();
AlbumOfMP3ViewModel albumOfMP3s = new AlbumOfMP3ViewModel
{
Album = albumFiles.First().Album,
Artist = albumFiles.First().Artist,
Files = albumFiles
};
albumOfMP3s.ObtainImageForAlbum();
albumOfMP3s.AnimationDelayMs = currentAnimationTime += animationOffset;
albums.Add(albumOfMP3s);
}
AlbumsReturned = albums;
Now, what Sonic does with these is within the MediaViewView
, there is an ItemsControl
that binds to this property of the AlbumsReturned
property of the MediaViewViewModel
; here is the XAML:
<ItemsControl x:Name="albumItems"
VerticalAlignment="Center" Height="130"
HorizontalAlignment="Stretch" Margin="0"
ItemsSource="{Binding MediaViewVM.AlbumsReturned}"
ItemTemplate="{StaticResource albumItemsTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
It can be seen that the ItemsControl
binds to the AlbumsReturned
property of the MediaViewViewModel
. That's cool. What does one of these AlbumOfMP3ViewModel
objects look like? Well, it is a ViewModel, so it is just a class. Here is the code. I should just mention that this ViewModel does some cool stuff to try and obtain the album art work. It basically examines the settings to see if it should search the hard drive for album art or do a Google search.
This is discussed in the Settings section in more detail.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.ComponentModel;
using System.Timers;
using Gapi.Search;
using System.IO;
namespace Sonic
{
[DebuggerDisplay("{ToString()}")]
public class AlbumOfMP3ViewModel : ViewModelBase
{
#region Data
private String album = String.Empty;
private String artist = String.Empty;
private List<MP3> files = new List<MP3>();
private String albumCoverArtUrl = String.Empty;
private Boolean isAnimatable = false;
private Double animationDelayMs = 500;
private Timer delayStartAnimationTimer = new Timer();
public event EventHandler<EventArgs>
AnimationStartTimerExpiredEvent;
private List<String>
allowableLocalImageFormats = new List<String>();
#endregion
public AlbumOfMP3ViewModel()
{
delayStartAnimationTimer.Enabled = true;
delayStartAnimationTimer.Elapsed += DelayStartAnimationTimer_Elapsed;
allowableLocalImageFormats.Add("*.jpg");
allowableLocalImageFormats.Add("*.png");
allowableLocalImageFormats.Add("*.gif");
}
#region Public Methods
public void OnAnimationStartTimerExpiredEvent()
{
EventHandler<EventArgs> temp = AnimationStartTimerExpiredEvent;
if (temp != null)
temp(this, new EventArgs());
}
public void StartDelayedAnimationTimer()
{
delayStartAnimationTimer.Start();
}
public Boolean ObtainImageForAlbum()
{
Boolean attemptToGainWebAlbumArt = false;
if (Boolean.TryParse(Sonic.Properties.Settings.
Default.AttemptToGainWebAlbumArt,
out attemptToGainWebAlbumArt));
if (attemptToGainWebAlbumArt)
{
Boolean foundValidImage = false;
String tempImageUrl = String.Empty;
WebClient webClient = new WebClient();
String downloadedContent = String.Empty;
try
{
SearchResults searchResults =
Searcher.Search(SearchType.Image,
String.Format("{0}", Album));
if (searchResults.Items.Count() > 0)
{
for (int i = 0; i < 1; i++)
{
downloadedContent =
webClient.DownloadString(searchResults.Items[i].Url);
if (!(downloadedContent.Contains("404") ||
downloadedContent.ToLower().
Contains("file not found")))
{
tempImageUrl = searchResults.Items[i].Url;
foundValidImage = true;
break;
}
else
{
foundValidImage = false;
break;
}
}
}
}
catch (WebException)
{
foundValidImage = false;
}
if (foundValidImage)
albumCoverArtUrl = tempImageUrl;
else
albumCoverArtUrl = "../Images/NoImage.png";
}
else
{
if (!FoundHardDiskImage())
{
albumCoverArtUrl = "../Images/NoImage.png";
}
}
return true;
}
#endregion
#region Private Methods
private void DelayStartAnimationTimer_Elapsed(
object sender, ElapsedEventArgs e)
{
delayStartAnimationTimer.Enabled = false;
delayStartAnimationTimer.Stop();
OnAnimationStartTimerExpiredEvent();
}
private Boolean FoundHardDiskImage()
{
try
{
FileInfo f = new FileInfo(files[0].FileName);
foreach (String allowableLocalImageFormat in
allowableLocalImageFormats)
{
String[] imageFiles =
Directory.GetFiles(f.Directory.FullName,
allowableLocalImageFormat);
if (imageFiles.Length > 0)
{
albumCoverArtUrl = imageFiles[0];
return true;
}
}
return false;
}
catch
{
albumCoverArtUrl = "../Images/NoImage.png";
return false;
}
}
#endregion
#region Public Properties
public Double AnimationDelayMs
{
private get { return animationDelayMs; }
set
{
animationDelayMs = value;
delayStartAnimationTimer.Interval = animationDelayMs;
}
}
public Boolean IsAnimatable
{
get { return isAnimatable; }
set
{
isAnimatable = value;
NotifyPropertyChanged("IsAnimatable");
}
}
public String Album
{
get { return album; }
set
{
album = value;
NotifyPropertyChanged("Album");
}
}
public String Artist
{
get { return artist; }
set
{
artist = value;
NotifyPropertyChanged("Artist");
}
}
public List<MP3> Files
{
get { return files; }
set
{
files = value;
NotifyPropertyChanged("Files");
}
}
public String AlbumCoverArtUrl
{
get { return albumCoverArtUrl; }
set
{
albumCoverArtUrl = value;
NotifyPropertyChanged("AlbumCoverArtUrl");
}
}
public String ToolTipDisplay
{
get { return ToString(); }
}
#endregion
#region Overrides
public override string ToString()
{
return String.Format(
"Album : {0}, Artist : {1}",
Album, Artist);
}
#endregion
}
}
Well, that's great. So how do we actually get to see some UI for this ViewModel? Well, if we re-examine the ItemsControl
XAML again.
<ItemsControl x:Name="albumItems"
VerticalAlignment="Center" Height="130"
HorizontalAlignment="Stretch" Margin="0"
ItemsSource="{Binding MediaViewVM.AlbumsReturned}"
ItemTemplate="{StaticResource albumItemsTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
We can see that there is an albumItemsTemplate ItemTemplate
template involved for each item. Let us have a look at one of those.
<DataTemplate x:Key="albumItemsTemplate">
<local:AlbumView DataContext="{Binding}"/>
</DataTemplate>
It can be seen for each item within the ItemsControl
(which is really an AlbumOfMP3ViewModel
) which allows us to show some UI for the bound item AlbumOfMP3ViewModel
.
This is a very powerful technique that allows us to basically slice and dice the UI into smaller parts that are all controlled by ViewModels.
I mentioned earlier that I wanted to support things like animation. Well, if we stick with this example AlbumOfMP3ViewModel
ViewModel and look at it in runtime, we will notice that each Item
in the ItemsControl
(which are really showing individual AlbumView
Views) is animated into position based on the associated ViewModel properties.
The AlbumOfMP3ViewModel
knows nothing about the View, but rather starts a timer off which, after it elapses, sets an AlbumOfMP3ViewModel
property which the AlbumView
knows about and so starts its own animation. Do you see, the ViewModel controls the View without even having to know about it? It may become clearer if we look at the code for the AlbumView
View.
Here is the XAML:
<UserControl x:Class="Sonic.AlbumView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Left"
Height="100" Width="100"
x:Name="userControl">
<UserControl.Resources>
<Storyboard x:Key="OnLoaded1">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="btn"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.1500000" Value="1.5"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1.25"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.50" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="btn"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.1500000" Value="1.5"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1.25"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.50" Value="1.0"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="btn"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00.05"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Button x:Name="btn" Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="Auto"
ToolTip="{Binding ToolTipDisplay}"
Click="btn_Click"
Template="{StaticResource GlassButton}"
RenderTransformOrigin="0.5,0.5">
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Button.RenderTransform>
<Image Margin="4" Source="{Binding AlbumCoverArtUrl}"
Stretch="UniformToFill"/>
</Button>
</UserControl>
And here is the code-behind for this View:
public delegate void AlbumClickedEventHandler(object sender,
AlbumClickedEventArgs e);
public partial class AlbumView : UserControl
{
public AlbumView()
{
InitializeComponent();
this.DataContextChanged+=AlbumView_DataContextChanged;
}
#region Events
public static readonly RoutedEvent AlbumClickedEvent =
EventManager.RegisterRoutedEvent(
"AlbumClicked", RoutingStrategy.Bubble,
typeof(AlbumClickedEventHandler),
typeof(AlbumView));
public event AlbumClickedEventHandler AlbumClicked
{
add { AddHandler(AlbumClickedEvent, value); }
remove { RemoveHandler(AlbumClickedEvent, value); }
}
#endregion
#region Private methods
private void AlbumView_DataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
AlbumOfMP3ViewModel viewModel = e.NewValue as AlbumOfMP3ViewModel;
if (viewModel != null)
{
viewModel.StartDelayedAnimationTimer();
viewModel.AnimationStartTimerExpiredEvent +=
ViewModel_AnimationStartTimerExpiredEvent;
}
}
private void ViewModel_AnimationStartTimerExpiredEvent(object sender, EventArgs e)
{
this.Dispatcher.InvokeIfRequired(() =>
{
Storyboard sb = this.TryFindResource("OnLoaded1") as Storyboard;
if (sb != null)
{
sb.Begin(this.btn);
}
}, DispatcherPriority.Normal);
}
private void btn_Click(object sender, RoutedEventArgs e)
{
AlbumClickedEventArgs args = new
AlbumClickedEventArgs(AlbumClickedEvent,
this.DataContext as AlbumOfMP3ViewModel);
RaiseEvent(args);
}
#endregion
}
AlbumView3D View
Simply shows 3D album art animation:
MainWindow View/ViewModel
Is the container for all other Views. Its ViewModel sets up the Genres/Artist letters, and has an IsBusy
state.
MediaView View/ViewModel
Is the main area within the MainWindow and hosts n-many AlbumView(s) and n-many MP3FileView(s).
MP3FileView View/ViewModel
Represents a single MP3 track.
All these additional View/ViewModels work in the way outlined above.
Sonic actually allows more music to be added even if the initial scan has been done. This is done via drag and drop, where the user is able to drag an entire directory or just single file(s). This is done by dragging items to the drag area of Sonic.
Now that you have a better understanding of the whole View/ViewModel pattern, I feel it's OK to assume you know that each View has a ViewModel that governs the View. The MainWindow
is no exception; it uses the MainWindowViewModel
, which has an IsBusy
/IsNotBusy
property.
What happens is that the MainWindow
examines the IsNotBusy
property of the MainWindowViewModel
, and if it is false
, will delegate the drag and drop functions to a DragAndDropHelper
helper class that actually does the drag/drop operations.
private void StackPanel_DragOver(object sender, DragEventArgs e)
{
if (mainWindowViewModel.IsNotBusy)
dragAndDropHelper.DragOver(e);
}
private void StackPanel_Drop(object sender, DragEventArgs e)
{
if (mainWindowViewModel.IsNotBusy)
dragAndDropHelper.Drop(e);
}
The basic idea is this; if the thing being dragged is a directory, all its files are scanned, and if they are not in the Sonic database, they are added to the database, but only if they are valid audio (MP3 only) files.
If the items are files, the process is as outlined above.
public enum FileType { Audio, NotSupported }
public class DragAndDropHelper
{
#region Public Methods
public void Drop(DragEventArgs e)
{
try
{
e.Effects = DragDropEffects.None;
string[] fileNames =
e.Data.GetData(DataFormats.FileDrop, true)
as string[];
if (Directory.Exists(fileNames[0]))
{
string[] files = Directory.GetFiles(fileNames[0]);
AddFilesToDatabase(files);
}
else
{
AddFilesToDatabase(fileNames);
}
}
catch
{
e.Effects = DragDropEffects.None;
}
finally
{
e.Handled = true;
}
}
public void DragOver(DragEventArgs e)
{
try
{
e.Effects = DragDropEffects.None;
string[] fileNames =
e.Data.GetData(DataFormats.FileDrop, true)
as string[];
if (Directory.Exists(fileNames[0]))
{
string[] files = Directory.GetFiles(fileNames[0]);
CheckFiles(files, e);
}
else
{
CheckFiles(fileNames, e);
}
}
catch
{
e.Effects = DragDropEffects.None;
}
finally
{
e.Handled = true;
}
}
public FileType GetFileType(string fileName)
{
string extension = System.IO.Path.GetExtension(fileName).ToLower();
if (extension == ".mp3")
return FileType.Audio;
return FileType.NotSupported;
}
#endregion
#region Private Methods
private void CheckFiles(string[] files, DragEventArgs e)
{
foreach (string fileName in files)
{
FileType type = GetFileType(fileName);
if (type == FileType.Audio)
e.Effects = DragDropEffects.Copy;
}
}
private void AddFilesToDatabase(String[] files)
{
SQLMP3sDataContext datacontext = new SQLMP3sDataContext();
try
{
foreach (string fileName in files)
{
FileType type = GetFileType(fileName);
if (type == FileType.Audio)
{
MP3 mp3File = XMLAndSQLQueryOperations.
ProcessSingleMP3File(fileName);
if (mp3File != null)
{
datacontext = new SQLMP3sDataContext();
if (datacontext.MP3s.Where(mp3 =>
mp3.Album == mp3File.Album).Count() > 0)
return;
datacontext.MP3s.InsertOnSubmit(mp3File);
datacontext.SubmitChanges();
}
}
}
}
catch (Exception ex)
{
}
}
#endregion
}
I have spent a lot of time and effort on this article, so if anyone out there votes less than 5, could they at least just tell me why? Was the technical content not right? Did you not like the article's layout etc.?
Anyway, that's it, comments are welcome.