What’s Coming to Windows Phone in 2011?

With more that 13,000 applications in their Marketplace Microsoft came out on stage today to make some new announcements about what’s coming to Windows Phone 7. We’ll see a major upgrade on all Windows Phone devices by the end of the year and developers are getting access to a lot of new APIs enabling new application scenarios.

Here is a summary list of the new features.

  • Multitasking – in addition to faster application switching multitasking will allow applications to continue processing in the background.
  • Live Tile Functionality Enhancements
  • Sensor Library Enhancements – You’ll have enhanced access to the sensor library and
      Access to the camera and compass-! Reality augmentation is possible!
    • Sockets – This needs no explanation
    • Database
    • IE9 – with hardware acceleration
    • Silverlight+XNA – you can use Silverlight and XNA in the same application
    • Twitter in the People Hub
    • Background Transfers
    • Profile
    • Silverlight 4 Runtime

    Several new countries are being added to the Marketplace. This brings up the total count from 17 to 35. The new countries are listed below with *.

    • Australia
    • Austria
    • Belgium
    • Brazil*
    • Canada
    • Chile*
    • Columbia*
    • Czech Republic*
    • Denmark*
    • Finland*
    • France
    • Germany
    • Greece*
    • Hong Kong
    • Hungary*
    • India*
    • Ireland
    • Italy
    • Japan*
    • Mexico
    • Netherlands*
    • New Zealand
    • Norway*
    • Poland*
    • Portugal*
    • Russia*
    • Singapore
    • South Africa*
    • South Korea*
    • Spain
    • Sweden*
    • Switzerland
    • Taiwan*
    • UK
    • USA /

Streaming from the Microphone to Isolated Storage

Last week I posted a sample voice recorder on CodeProject. The application would buffer the entire recording in memory before writing it to a file. A rather astute reader asked me what would happen if the user let the recording go long enough to fill up memory. The answer to that question is the application would crash due to an exception being trhown when it fails to allocate more memory and all of the recordingwould be lost. I had already been thinking of a sime reusable solution for doing this but I also offered to the user the following code sample to handle streaming directly to IsolatedStorage.
My two goals in writing it were to keep it simple and keep it portable/reusable. As far as usage goes I can’t think of any ways to make it any easier.
   //To start a recording
   StreamingRecorder myRecorder  = new StreamingRecorder();
   myRecorder.Start("myFileName");

  //To stop a recording();
  myRecorder.Stop();
After the code has run you will have a WAVE file with a proper header ready to be consumed by a SoundEffect, MediaElement, or whatever it is that you want to do with it.

In implementing this I must say that I have a hiher appreciation for how MediaElement‘s interface is designed. The starting and stopping process are not immediate. In otherwords when you call Start() or Stop() it is not until a few moments later that the request is fully processed. Because of the asynchronous nature of these processes I’ve implemented the event RecordingStateChanged and the property RecordingState so that I would know when a state change was complete. If you are familiar with the media element class then your recognize the similarity of this pattern.
I’ll go into further details on how this works along with implemeting some other functionality (such as a Pause method) in a later post. But the code is in a working state now so I’m sharing it. 🙂
Here is the source:
public class StreamingRecorder :INotifyPropertyChanged,  IDisposable
{


    object SyncLock = new object();

    private Queue<MemoryStream> _availablBufferQueue;        
    private Queue<MemoryStream> _writeBufferQueue;

    private int _bufferCount;
    private byte[] _audioBuffer;

    //private int _currentRecordingBufferIndex;
        

    private TimeSpan _bufferDuration;
    private int _bufferSize;
    private Stream _outputStream;
    private Microphone _currentMicrophone;
    private bool _ownsStream = false;
    private long _startPosition;

    

    public  StreamingRecorder(TimeSpan? bufferDuration = null, int bufferCount=2)
    {
        _bufferDuration = bufferDuration.HasValue ? bufferDuration.Value : TimeSpan.FromSeconds(0);
        _bufferCount = bufferCount;
        _currentMicrophone= Microphone.Default;   
    }

    private MemoryStream CurrentBuffer
    {
        get; set;
    }

    public void Start(string fileName)
    {
        var isoStore = System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication();
        var targetFile = isoStore.OpenFile(fileName, FileMode.Create);
        Start(targetFile, true);
    }

    public void Start(Stream outputStream, bool ownsStream=false)
    {
        _outputStream = outputStream;
        _ownsStream = ownsStream;
        _startPosition = outputStream.Position;

        Size = 0;

        //Create our recording buffers
        _availablBufferQueue = new Queue<MemoryStream>();
        _writeBufferQueue = new Queue<MemoryStream>();
        _audioBuffer = new byte[_currentMicrophone.GetSampleSizeInBytes(_currentMicrophone.BufferDuration)];
        _bufferSize = _currentMicrophone.GetSampleSizeInBytes(_bufferDuration + _currentMicrophone.BufferDuration);
        for (var i = 0; i < _bufferCount; ++i)
        {
            _availablBufferQueue.Enqueue(new MemoryStream(_bufferSize));
        }

        CurrentBuffer = _availablBufferQueue.Dequeue();
        //Stuff a bogus wave header in the output stream as a space holder.
        //we will come back and make it valid later. For now the size is invalid.
        //I could have just as easily stuffed any set of values here as long as 
        //the size of those values equaled 0x2C
        WaveHeaderWriter.WriteHeader(CurrentBuffer, -1, 1, _currentMicrophone.SampleRate);
        Size += (int)CurrentBuffer.Position;

        //Subscribe to the Microphone's buffer ready event and start listening.
        _currentMicrophone.BufferReady += new EventHandler<EventArgs>(_currentMicrophone_BufferReady);            
        _currentMicrophone.Start();
    }


    void _currentMicrophone_BufferReady(object sender, EventArgs e)
    {
        _currentMicrophone.GetData(_audioBuffer);
        //If the recorder is paused (not implemented) then don't add this audio chunk to
        // the output. If HasFlushed is set then the recording is actually ready to shut
        //down and we shouldn't accumulate anything more. 
        if ((CurrentState != RecordingState.Paused))
        {
            //Append the audio chunk to our current buffer
            CurrentBuffer.Write(_audioBuffer, 0, _audioBuffer.Length);
            //Increment the size of the recording.
            Size += _audioBuffer.Length;
            //If the buffer is full or if we are shutting down then we need to submit
            //the buffer to be written to the output stream.
            if ((CurrentBuffer.Length > _bufferSize)||(CurrentState==RecordingState.Stopping))
            {

                SubmitToWriteBuffer(CurrentBuffer);
                //If we were shutting down then set a flag so that it is known that the last audio
                //chunk has been written. 
                if (CurrentState == RecordingState.Stopping)
                {
                    _currentMicrophone.Stop();
                    _currentMicrophone.BufferReady -= _currentMicrophone_BufferReady;
                }
                CurrentBuffer = _availablBufferQueue.Count > 0 ? _availablBufferQueue.Dequeue() : new MemoryStream();
            }
        }
    }

                

    // CurrentState - generated from ObservableField snippet - Joel Ivory Johnson

    private RecordingState _currentState;
    public RecordingState CurrentState
    {
        get { return _currentState; }
        set
        {
            if (_currentState != value)
            {
                _currentState = value;
                OnPropertyChanged("CurrentState");
                OnRecordingStateChanged(value);
            }
        }
    }
    //-----


    void WriteData(object a )
    {

        lock(SyncLock)
        {                
            while (_writeBufferQueue.Count > 0)
            {
                var item = _writeBufferQueue.Dequeue();
                var buffer = item.GetBuffer();
                _outputStream.Write(buffer, 0,(int) item.Length);
                item.SetLength(0);

                _availablBufferQueue.Enqueue(item);

                if (CurrentState == RecordingState.Stopping)
                {
                    //Correct the information in the wave header. After it is
                    //written set the file pointer back to the end of the file.
                    long prePosition = _outputStream.Position;
                    _outputStream.Seek(_startPosition, SeekOrigin.Begin);
                    WaveHeaderWriter.WriteHeader(_outputStream,Size-44,1,_currentMicrophone.SampleRate);
                    _outputStream.Seek(prePosition, SeekOrigin.Begin);
                    _outputStream.Flush();
                    if (_ownsStream)
                        _outputStream.Close();
                    CurrentState = RecordingState.Stopped;
                }
            }
        }
    }

    void SubmitToWriteBuffer(MemoryStream target)
    {
        //Do the writing on another thread so that processing on this thread can continue. 
        _writeBufferQueue.Enqueue(target);
        ThreadPool.QueueUserWorkItem(new WaitCallback(WriteData));
    }

    public void Pause()
    {
        if ((CurrentState != RecordingState.Paused) && (CurrentState != RecordingState.Recording))
        {
            throw new Exception("you can't pause if you are not recording");
        }
        CurrentState = RecordingState.Paused;
    }

    public void Stop()
    {
        CurrentState = RecordingState.Stopping;
    }


    // Size - generated from ObservableField snippet - Joel Ivory Johnson

    private int  _size;
    public int Size
    {
        get { return _size; }
        set
        {
            if (_size != value)
            {
                _size = value;
                OnPropertyChanged("Size");
            }
        }
    }
    //-----

    public long RemainingSpace
    {
        get
        {                
            return System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication().AvailableFreeSpace;
        }
    }

    public TimeSpan RecordingDuration
    {
        get
        {
            return _currentMicrophone.GetSampleDuration((int)Size);
        }
    }

    public TimeSpan RemainingRecordingTime
    {
        get
        {
            return _currentMicrophone.GetSampleDuration((int)RemainingSpace);
        }
    }

    //-------

    public event EventHandler<RecordingStateChangedEventArgs> RecordingStateChanged;
    protected void OnRecordingStateChanged(RecordingState newState)
    {
        if(RecordingStateChanged!=null)
        {
            RecordingStateChanged(this, new RecordingStateChangedEventArgs(){NewState = newState});
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public void Dispose()
    {
        Stop();
    }
}

 

Voice Memo Source for WP7

This question keeps coming up in the forums so I decided to put the application together and make it publically available. If you head over to CodeProject you’ll find a small article that I uploaded on making a voice memo application on Windows Phone 7. Among other things is demonstrates how to convert the raw recording bytes to a proper wave file, simple serialization, and a few other tid bits. For the sake of the article I did send the code through certification.

However the application looks ugly right now. I’ve got a graphic artist that I’ll be paying to design the UI for me and since I’m paying her for this I’ve decided not to include the graphic assets that she is producing in the code that I’m gicing away for free.

There’s no obligations attached to the code. But if you use it in your own products I would appreciate a heads up just so that I know where it’s being used.

Share photos on twitter with Twitpic

Changing the Background on a Button

A recent question in the Windows Phone Development Forums asking for the XAML to display a background image in a button when it is pressed. Generating the XAML to do this is pretty easy (if you know how!). While the request was for the XAML for doing this I thought the instructions for producing the XAML to do this are of great value.

Open expressions blend and start a new project. In your project add a new button. Right-click on the button and select “Edit template”->”Edit Copy” You will be prompted for the name of the new button style that we are creating (call it what you want) and whether the template will be defined in the document (page) or defined globally for the application. If you only plan on using the style in one page then it’s fine to define it within the document. In general you are probably going to use your style on more than one page. In either case for this exercise select the option to create the style within the document.

Switch to code view so that we can edit the XAML. Towards the top of your document you will see a style defined with the name you gave to it. Scroll down within the style until you find the construct with a ContentControl enveloped within a Border element. It will look like the following.

<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" 
            CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}">								
     <ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" 
                             Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" 
                             HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" 
                             Padding="{TemplateBinding Padding}" 
                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>

We are going to make most our changes here. We need to place an image within this border element. It is going to be behind the content and so it will have to appear before the ContentControl element. The Border element can only have one child so we will need to make a Grid the Border‘s direct child and then place the Image element and ContentControl element within the Grid. The Image attribute will need to have a name and it will need to have its Opacity set to zero since the image usually will not be visible. The resulting XAML will look like the following.

<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}">								
	<Grid>
		<Image x:Name="BackgroundImage" Source="/Background.png" 
                            Stretch="Fill" VerticalAlignment="Bottom" Opacity="0" />
		<ContentControl x:Name="ContentContainer" 
                                       ContentTemplate="{TemplateBinding ContentTemplate}" 
                                       Content="{TemplateBinding Content}" 
                                       Foreground="{TemplateBinding Foreground}" 
                                       HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                       Padding="{TemplateBinding Padding}" 
                                       VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
	</Grid>
</Border>

Almost done! There’s only a couple of things left. We want the image to be visible when the user is pressing the button. Scroll up within the template and you’ll find several VisualStateGroup elements defined. This area contains the changes and transitions that need to occur on the button when certain things happen such as the button going to a disabled state, loosing or gaining focus, and so on. We are interested in changes that occur in the pressed state. Within the VisualState named Press is a StoryBoard containing several animations. We need to add one more animation that changes the opacity of our button. As the last child of the StoryBoard element add the following.

<DoubleAnimation To="100" Duration="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundImage" />

Now if you run the project you’ll see the image show up in the button any time you press it, and disappear when ever you release it. Now how do you use this in another project? If you copy the Style element from this project and place it as a resource within your other projects it will be readily available for you (just make sure that your image source also appears in your target project). The style can be applied to a button through setting the buttons style.

<Button  Style="{StaticResource MyCustomButton}"/>

If you want to see what my entire style looks like here it is.

<Style x:Key="MyCustomButton" TargetType="Button">
	<Setter Property="Background" Value="Transparent"/>
	<Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>
	<Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
	<Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
	<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>
	<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
	<Setter Property="Padding" Value="10,3,10,5"/>
	<Setter Property="Template">
		<Setter.Value>
			<ControlTemplate TargetType="Button">
				<Grid Background="Transparent">
					<VisualStateManager.VisualStateGroups>
						<VisualStateGroup x:Name="CommonStates">
							<VisualState x:Name="Normal"/>
							<VisualState x:Name="MouseOver"/>
							<VisualState x:Name="Pressed">
								<Storyboard>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
										<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneBackgroundBrush}"/>
									</ObjectAnimationUsingKeyFrames>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
										<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
									</ObjectAnimationUsingKeyFrames>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
										<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneForegroundBrush}"/>
									</ObjectAnimationUsingKeyFrames>
									<DoubleAnimation To="100" Duration="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundImage" />
								</Storyboard>
							</VisualState>
							<VisualState x:Name="Disabled">
								<Storyboard>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer">
										<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
									</ObjectAnimationUsingKeyFrames>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="ButtonBackground">
										<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}"/>
									</ObjectAnimationUsingKeyFrames>
									<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="ButtonBackground">
										<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>
									</ObjectAnimationUsingKeyFrames>
								</Storyboard>
							</VisualState>
						</VisualStateGroup>
					</VisualStateManager.VisualStateGroups>
					<Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}">								
						<Grid>
							<Image x:Name="BackgroundImage" Source="/Background.png" Stretch="Fill" VerticalAlignment="Bottom" Opacity="0" />
							<ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
						</Grid>
					</Border>
					
				</Grid>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
</Style>

Tracking High Scores on Windows Phone

Another frequent question I come across in the user forums is related to how some one implements local high scores. The question has come up frequently enough for me to conclude that its to the benifit of the community to have an implementation available that can be used in Silverlight or XNA that is ready to be used with very little setup.

So I’ve made a solution for others to use. By default the component will keep track of up to 10 high scores and will take care of loading and saving itself. If you add a score the component will take care of ensuring the score is in it’s proper place and removing scores that are nolonger one of the top. For persisting the score information I’ve made use of the DataSaver<T> code from a previous blog post. I hope others will find the solution easy to use.

To get started with using the component add a reference to my component to your project. You’ll want to instantiate HighScoreList passing an optional file name that it will use to save score information. It’s possible to keep track of more than one high score list as long as your instances have different file names. One might want to do this if they keep track of scores in different modes separately from each other (Ex: a score list for Difficult mode, a score list for Easy mode, and so on).

HighScoreList _highScoreList = new HighScoreList("MyScores");

Upon instantiation the component will take care of loading any previous high scores without you doing anything more.

To add a score create a new instance of ScoreInfo and populate its PlayerName and Score fields. (There is also a ScoreDate field that automatically gets populated with the current date and time). Then use the AddScore(ScoreInfo) method on the HighScoreList instance to add it to the score list.

ScoreInfo scoreInfo = new ScoreInfo(){PlayerName = "Jack", Score = 1048576};
_highScoreList.AddScore(scoreInfo);

And that’s it, there’s nothing more for you to do. When you make that call the score gets added to the high score list, scores that are no longer in the top 10 (or what ever you set the limit to be) will fall off the list, and the list will automatically be persisted back to IsolatedStorage so that it is available the next time your game runs. Easy, right?

As a test project I’ve created a Silverlight application that allows you to enter new scores and see the behaviour of the component.

Score Keeper Screenshot

The main bits of the source code are below. First the ScoreInfo class which is nothing more than a serializable collection of three properties

/// <summary>
/// ScoreInfo contains information on a single score
/// </summary>
[DataContract]
public class ScoreInfo : INotifyPropertyChanged
{

    // PlayerName - generated from ObservableField snippet - Joel Ivory Johnson
        private string _playerName = String.Empty;

    /// <summary>
    /// The name of the player that made this score
    /// </summary>
        [DataMember]
        public string PlayerName
        {
        get { return _playerName; }
            set
            {
                if (_playerName != value)
                {
                    _playerName = value;
                    OnPropertyChanged("PlayerName");
                }
            }
        }
        //-----

    // Score - generated from ObservableField snippet - Joel Ivory Johnson
        private int _score = 0;

    /// <summary>
    /// The score that the player made
    /// </summary>
        [DataMember]
        public int Score
        {
        get { return _score; }
            set
            {
                if (_score != value)
                {
                    _score = value;
                    OnPropertyChanged("Score");
                }
            }
        }
        //-----

    // ScoreDate - generated from ObservableField snippet - Joel Ivory Johnson
        private DateTime _scoreDate = DateTime.Now;

    /// <summary>
    /// The date and time that the player made the score. If this field is not
    /// assigned a value it will automatically be assigned with the date and time
    /// that the score isntance was created
    /// </summary>
        [DataMember]
        public DateTime ScoreDate
        {
        get { return _scoreDate; }
            set
            {
                if (_scoreDate != value)
                {
                    _scoreDate = value;
                    OnPropertyChanged("ScoreDate");
                }
            }
        }
        //-----
    protected void OnPropertyChanged(String propertyName)
    {
        if(PropertyChanged!=null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #region INotifyPropertyChanged Members

    public  event PropertyChangedEventHandler PropertyChanged;

    #endregion
}

And then the HighScoreList class, which is a collection class that contains the .

using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.Serialization; namespace J2i.Net.ScoreKeeper { public class HighScoreList : ObservableCollection<ScoreInfo>, INotifyPropertyChanged { static DataSaver<HighScoreList> MyDataSaver = new DataSaver<HighScoreList>(); public HighScoreList() { } public HighScoreList(string fileName):this() { this.ScoreFileName = fileName; HighScoreList temp = MyDataSaver.LoadMyData(fileName); if(temp!=null) { foreach(var item in temp) { Add(item); } } } // MaxScoreCount - generated from ObservableField snippet - Joel Ivory Johnson private int _maxScoreCount = 10; [DataMember] public int MaxScoreCount { get { return _maxScoreCount; } set { if (_maxScoreCount != value) { _maxScoreCount = value; OnPropertyChanged("MaxScoreCount"); } } } //----- // ScoreFileName - generated from ObservableField snippet - Joel Ivory Johnson private string _scoreFileName = "DefaultScores"; [DataMember] public string ScoreFileName { get { return _scoreFileName; } set { if (_scoreFileName != value) { _scoreFileName = value; OnPropertyChanged("ScoreFileName"); } } } //----- // AutoSave - generated from ObservableField snippet - Joel Ivory Johnson private bool _autoSave = true; [DataMember] public bool AutoSave { get { return _autoSave; } set { if (_autoSave != value) { _autoSave = value; OnPropertyChanged("AutoSave"); } } } //----- static int ScoreComparer(ScoreInfo a, ScoreInfo b) { return b.Score - a.Score; } public void SortAndDrop() { List<ScoreInfo> temp = new List<ScoreInfo>(this.Count); foreach(var item in this) { temp.Add(item); } if (temp.Count > MaxScoreCount) { temp.RemoveRange(MaxScoreCount - 1, (temp.Count) - (MaxScoreCount)); } temp.Sort(ScoreComparer); this.Clear(); temp.ForEach((o)=>Add(o)); } public void Save() { if(String.IsNullOrEmpty(ScoreFileName)) throw new ArgumentException("A file name wasn't provided"); MyDataSaver.SaveMyData(this, ScoreFileName); } public void AddScore(ScoreInfo score) { this.Add(score); SortAndDrop(); if(AutoSave) Save(); } protected void OnPropertyChanged(String propertyName) { if(PropertyChanged!=null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion } }