Tuesday, 11 September 2007

Mix:UK 07 - Schedule

It's late and I should be in bed for tomorrow, but I realized the conference center is a whopping ten minutes walk from my home sweet home, so I have a bit of time. Thought I may as well let people know which sessions I'll be watching and reporting about. As you can tell, I'm mostly interested in WP, the DLR and silverlight 1.1.

Day 1

09:30 - 11:00 Mix:UK 07 Keynote
11:30 - 12:30 Designing immersive experiences with Expression Blend, WPF and Silverlight
13:15 - 14:15 Building Silverlight applications using .net Part 2 (to be confirmed)
14:30 - 15:30 Building next generation web applications using Windows Live Services
16:00 - 17:00 Developer Panel

But of course I may change my mind later. Off to bed now, see you all tomorrow. Let's hope wi-fi works alright!

Monday, 10 September 2007

Mix:UK 07 tomorrow

How thrilling! Hopefully I'll be able to blog about it. Wonder if I can get my camcorder with me...

Technorati Tags:

Monday, 3 September 2007

WPF Tips'n'Tricks #6: Preventing ScrollViewer from handling the mouse wheel

In the category of the pot talking to the pan, I present you ScrollViewer. It's the main control to implement scrolling in your templates, but it's also the one not respecting a very  fundamental rule of scrolling: if you're done scrolling, let your parent scroll!

Not only does ScrollViewer handles the mouse scrolling even when no more scrolling is needed, but it also does so when there's nothing to scroll, or worse when it is told not to scroll! Let's take an example XAML file. 

<Window x:Class="CaffeineIT.Blog.ScrollViewerExample.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="423" Width="596">

    <Grid>

        <ScrollViewer>

            <StackPanel>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <ScrollViewer Name="NoScrollingScrollViewer">

                    <TextBlock>Content that doesn't need scrolling</TextBlock>

                </ScrollViewer>

                <ScrollViewer Height="235" Name="ScrollingNeededScrollViewer">

                    <StackPanel>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <ListView>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                            <TextBlock>Inside Thrid ListView</TextBlock>

                        </ListView>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                        <TextBlock>Inside Second ScrollViewer</TextBlock>

                    </StackPanel>

                </ScrollViewer>

 

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

                <TextBlock>Inside First ScrollViewer</TextBlock>

            </StackPanel>

        </ScrollViewer>

    </Grid>

</Window>

How can we change the ScrollViewer to behave more like it's supposed to? The most direct approach is to leverage the tunneling and bubbling events and use them against the buggy control.

The idea is that if the PreviewMouseWheel is handled, WPF will not generate the MouseWheel event, and in turn the ScrollViewer will not scroll.

Let's add a handler for the PreviewMouseWheel event on one of our ScrollViewers.

<ScrollViewer Height="235" Name="ScrollingNeededScrollViewer" PreviewMouseWheel="HandlePreviewMouseWheel">

 

        private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)

        {

            if (sender is ScrollViewer && !e.Handled)

            {

                e.Handled = true;

                var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);

                eventArg.RoutedEvent = UIElement.MouseWheelEvent;

                eventArg.Source = sender;

                var parent = ((Control)sender).Parent as UIElement;

                parent.RaiseEvent(eventArg);

            }

        }

This does exactly what we want. It marks the tunneling PreviewMouseWheel event as handled, so as to prevent WPF from raising the bubbling MouseWheel event, which is the one causing the actual scrolling. This is fine in case you don't want a ScrollViewer to scroll at all and let its parent do the scrolling, but what if you only want your ScrollViewer to scroll until it cannot anymore, and then let the parent scroll (this is the behavior in Internet Explorer)? Let's tweak the code a bit.

        private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)

        {

            var scrollControl = sender as ScrollViewer;

            if (!e.Handled && sender != null)

            {

 

                bool cancelScrolling = false;

 

                if ((e.Delta > 0 && scrollControl.VerticalOffset == 0)

                    || (e.Delta <= 0 && scrollControl.VerticalOffset >= scrollControl.ExtentHeight - scrollControl.ViewportHeight))

                {

                    e.Handled = true;

                    var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);

                    eventArg.RoutedEvent = UIElement.MouseWheelEvent;

                    eventArg.Source = sender;

                    var parent = ((Control)sender).Parent as UIElement;

                    parent.RaiseEvent(eventArg);

                }

            }

        }

Now, we check on every mouse wheel scroll if any content needs scrolling in the direction the wheel was scrolled. We check the VerticalOffset property, as it is 0 when you can't scroll up anymore and ExtentHeight-ViewportHeight when you can't scroll down anymore. If there's nothing to scroll, we cancel the event and re-raise it just like we did before.

That's all well so far, but what if I have another child ScrollViewer, like the ListView in our example? The ListView will not receive any notifications if the parent ScrollViewer is scrolled to the max in either direction, because we stop the PreviewMouseWheel before it can reach the ListView. We need to change the code a bit more and do the work the framework would've done.

        private void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)

        {

            var scrollControl = sender as ScrollViewer;

            if (!e.Handled && sender != null && !_reentrantList.Contains(e))

            {

                var previewEventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)

                {

                    RoutedEvent = UIElement.PreviewMouseWheelEvent,

                    Source = sender

                };

                var originalSource = e.OriginalSource as UIElement;

                _reentrantList.Add(previewEventArg);

                originalSource.RaiseEvent(previewEventArg);

                _reentrantList.Remove(previewEventArg);

                // at this point if no one else handled the event in our children, we do our job

 

 

                if (!previewEventArg.Handled && ((e.Delta > 0 && scrollControl.VerticalOffset == 0)

                    || (e.Delta <= 0 && scrollControl.VerticalOffset >= scrollControl.ExtentHeight - scrollControl.ViewportHeight)))

                {

                    e.Handled = true;

                    var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);

                    eventArg.RoutedEvent = UIElement.MouseWheelEvent;

                    eventArg.Source = sender;

                    var parent = ((Control)sender).Parent as UIElement;

                    parent.RaiseEvent(eventArg);

                }

            }

        }

The main difference is that before we try to cancel the PreviewMouseWheel event by marking it Handled, we check if any child of the control would mark it Handled before us, which by WPF design would mean we shouldn't handle the event at all.

If you try this example now, you'll notice now that our ListView still prevents the scrolling to happen properly. That's because we only changed the behavior of the ScrollViewer we attached an event handler to, and not the one inside the ListView. Using the attached property initialization hack we used before, we can define an attached property that will do all the hookup work whenever attached to a ScrollViewer.

    public class ScrollViewerCorrector

    {

 

 

        public static bool GetFixScrolling(DependencyObject obj)

        {

            return (bool)obj.GetValue(FixScrollingProperty);

        }

 

        public static void SetFixScrolling(DependencyObject obj, bool value)

        {

            obj.SetValue(FixScrollingProperty, value);

        }

 

        public static readonly DependencyProperty FixScrollingProperty =

            DependencyProperty.RegisterAttached("FixScrolling", typeof(bool), typeof(ScrollViewerCorrector), new FrameworkPropertyMetadata(false,ScrollViewerCorrector.OnFixScrollingPropertyChanged));

 

        public static void OnFixScrollingPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

            ScrollViewer viewer = sender as ScrollViewer;

            if (viewer == null)

                throw new ArgumentException("The dependency property can only be attached to a ScrollViewer", "sender");

 

            if ((bool)e.NewValue == true)

                viewer.PreviewMouseWheel += HandlePreviewMouseWheel;

            else if ((bool)e.NewValue == false)

                viewer.PreviewMouseWheel -= HandlePreviewMouseWheel;

        }

        private static List<MouseWheelEventArgs> _reentrantList = new List<MouseWheelEventArgs>();

        private static void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e)

        {

            var scrollControl = sender as ScrollViewer;

            if (!e.Handled && sender != null && !_reentrantList.Contains(e))

            {

                var previewEventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)

                {

                    RoutedEvent = UIElement.PreviewMouseWheelEvent,

                    Source = sender

                };

                var originalSource = e.OriginalSource as UIElement;

                _reentrantList.Add(previewEventArg);

                originalSource.RaiseEvent(previewEventArg);

                _reentrantList.Remove(previewEventArg);

                // at this point if no one else handled the event in our children, we do our job

 

 

                if (!previewEventArg.Handled && ((e.Delta > 0 && scrollControl.VerticalOffset == 0)

                    || (e.Delta <= 0 && scrollControl.VerticalOffset >= scrollControl.ExtentHeight - scrollControl.ViewportHeight)))

                {

                    e.Handled = true;

                    var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);

                    eventArg.RoutedEvent = UIElement.MouseWheelEvent;

                    eventArg.Source = sender;

                    var parent = (UIElement)((FrameworkElement)sender).Parent;

                    parent.RaiseEvent(eventArg);

                }

            }

        }

    }

And the only thing left to do is to change the template for ScrollViewer to always define the attached property by adding the Style to the resources on the Window, and pronto, all your ScrollViewers are now behaving properly.

<Window x:Class="CaffeineIT.Blog.ScrollViewerExample.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="423" Width="596" xmlns:my="clr-namespace:CaffeineIT.Blog.ScrollViewerExample">

    <Window.Resources>

        <Style TargetType="{x:Type ScrollViewer}">

            <Style.Setters>

                <Setter Property="my:ScrollViewerCorrector.FixScrolling" Value="True" />

            </Style.Setters>

        </Style>

    </Window.Resources>

As usual, you can download the ScrollViewerFixer.zip code and sample.

Friday, 31 August 2007

Google adSense

After having adSense for months (and a total earning of $0.18), the ads are finally showing up something connected with my blog:

  • DataGrid controls from Infragistics
  • The power of .net 3.0 from Xceed
  • Debugging Visual Studio from Microsoft
  • Automating your page composition from Dakota... Uh?

Three out of four is not bad, and it's three companies any .net 3 developer should visit quite often for the latest news, so the ads are in this instance quite useful.

As for automating my page... This is a blog?!? Oh well.

Technorati Tags: , ,

Thursday, 30 August 2007

Programming WPF 2nd Edition is out

Well through Chris Sell's blog, http://www.sellsbrothers.com/news/showTopic.aspx?ixTopic=2122, the new edition is shipping. I liked the previous version, only real reference at the time I started WPF, together with the Petzold.

So I just placed an order for it, and at the same time ordered Nathan's and the other Chris book, so I can have a go at reading them all.

I'll do a comparison of all of them and report here. It's been done before but now that I've been working professionally for what seems like years but is in fact only a year (between the RTM and beta 2), I think I may see things differently.

It will also be fun to compare the first and the 2nd edition of Programming WPF :)

Question is, should I order the 3D Petzold? We'll see after I'm done with the rest.

Anyone wants to commission me to write "Writing controls for WPF: Put some sparkle in your cider!"?

Technorati Tags: , , , ,

Wednesday, 22 August 2007

Funny comment of the day

While coding, found a snippet one of my predecessors left me:

//  Media Player gets its

// knickers in a twist.

Thought it was hilarious!

Friday, 17 August 2007

Using Visual Studio 2008 and Scrum for Team System

If you're using Scrum for Team System and Visual Studio 2008, you may notice that when connecting to a newly created Scrum Team Project, the Work Items node gets a desperate looking red cross.

The solution is to simply connect to TFS using the 2005 Team Explorer. From there, create a new Release. Restart Visual Studio 2008 and your Team Queries will finally show up!

Thursday, 16 August 2007

Xceed DataGrid for WPF v1.2

 We've used the WPF DataGrid successfully on the National Express project, and the guys at Xceed are very prompt to answer questions and very quick to fix bugs we encountered with the first version of their products.

I'm being told it's now top quality and probably the best DataGrid available. And it's just been updated! Check their News Headlines page for the new features.

Technorati Tags: , , , ,

Wednesday, 15 August 2007

Some TFS links

Not the freshest of links but useful if you use TFS.

TFS Bug Snapper v1.0 Released

and

www.scrumforteamsystem.com that provides similar functionality to the eScrum tool I talked about before.

Tuesday, 14 August 2007

Quick note for testers

A quick link to Anutthara's blog because it's an invaluable resource for any tester. I'll probably enforce reading it to any tester I have to work with.

Install windows components on a locked-down machine

More and more companies set their users as non admin, even with XP. The so-called locking down tries to protect the user and the network against anything only an administrator should do.

But there's also group policy that directs what gets shown in windows, which software gets activated, etc. In my case, the policy doesn't let you access the Add/Remove Windows Components section.

Being an administrator on the machine, I could go and change the registry to deny read permission to whichever account is being used for policies. That's a bit drastic and I wouldn't recommend it.

But thanks to the complexity and myriad of options available to group policy, sometimes you can get away with easier things.

To add or remove windows components, open PowerShell and type the following.

PS C:\WINDOWS> sysocmgr /i:$env:windir\inf\sysoc.inf

Or for people still using cmd

C:\WINDOWS> sysocmgr /i:%WINDIR%\inf\sysoc.inf

I'd rather we didn't have to play catch and seek with network administrators, but you have to do what you have to do to get the job done sometimes.

Technorati Tags: , ,

Thursday, 9 August 2007

Mix:UK 07 - I'll be there!

Well, it's now official, I'll be at Mix:Uk 07!

Hope to get chatting with many of you about all things WPF / Silverlight. So much cross-over and so many differences!

DirectShow filters from MediaElement

I previously pointed to Jeremiah blog about his and Leslie's amazing win32 integration work.

Little did I realize that the trick I read a while ago to build your own DirectShow filter for a MediaElement control was also his!

Respect.

Wednesday, 8 August 2007

On why you shouldn't really subclass through XAML

As I am reviewing some code at the moment, I have been looking for this blog entry from Rob for a while, and just found it again: Building a control which holds content: CustomControl vs. MarkupSubclassing vs. UserControl. Included in my specification references, but posting it so I can reuse it as a reference.

And the ever so useful Neil (when are you getting a blog back?) points me to an article by Kevin on the same subject: Don't subclass a Panel, unless you're making a Panel.

It always amazes me that people, after x years of OOD, still don't see a problem in using inheritance instead of combination / encapsulation.

Then again these guidelines postdate most of the uses I've seen so far so you can't really blame anyone.

Technorati Tags: , , ,

Tuesday, 7 August 2007

Documentation != Help, or why visual studio sucks

Visual Studio 2005 has the terrible habit of opening the full msdn library whenever you press F1, which in my case is mostly accidental. Not that I don't need help, but the F1 is a reflex I use when I don't understand the meaning of an option on the UI. I couldn't care less for msdn at that point.

Visual Studio 2008 seems to have a faster <cough> <cough> documentation explorer, but goes into what I'd consider to be pure vice: The little question mark box next to the close button is for bloody contextual help. Guess what, opening your big document explorer in my face when I'm already trying to understand what in the name of god you meant by allow checked-in items to be edited is not contextual. I loose my patience, and definitely loose my respect for whichever manager decided that after all, contextual help could be in the big documentation.

Can I have my tooltips back? Please?

Thursday, 2 August 2007

WPF Tips'n'Tricks #5: Receive notifications for dependency properties

Receiving notifications for dependency property changes on an existing object is a very common scenario. The way to do it properly is not very obvious. So much so that while reviewing some code, I found the following snippet.

// Believe it or not, this seems to be the only way to get change
// notifications for DPs unless you derive from the relevant
// class and override OnPropertyChanged.

PropertyDescriptor prop = TypeDescriptor.GetProperties(obj)["Prop"];
prop.AddValueChanged(obj, delegate { viewModel.RaisePropertyChanged("Prop"); });

There's a few issues with this code. The first one is that you reflect on the CLR property anchoring the dependency property, and not the dependency property itself. For example, the following code wouldn't work.

public Dock Dock { get { return DockPanel.GetDock(this); } }
public void TestDockProperty()
{
    PropertyDescriptor descriptor = TypeDescriptor.GetProperties(this)["Dock"];
    descriptor.AddValueChanged(this, delegate(object sender, EventArgs args) { MessageBox.Show("ValueChanged!"); });

    DockPanel.SetDock(this, Dock.Top);
}

The reason is that the PropertyDescriptor points to the CLR property, not to the dependency property.

The second issue is a problem of performance. TypeDescriptor.GetProperties reflects on every call and doesn't cache the result, so its cost is O(n). Here's the result of iterating several times on the code using TypeDescriptor.

  • 1,000 iterations : 00:00:00.2811402
  • 10,000 iterations : 00:00:01.4369388
  • 100,000 iterations : 00:00:14.1194856

So what is the correct way to do it? Say hi to DependencyTypeDescriptor. Here's the code rewritten to use DependencyProperties.

DependencyPropertyDescriptor prop = DependencyPropertyDescriptor.FromProperty(ParentObject.PropProperty, obj.GetType());
            prop.AddValueChanged(obj, delegate { viewModel.RaisePropertyChanged("Prop"); });

If you execute the code in the small benchmark application we used previously, the results are completely different.

  • 1,000 iterations : 00:00:00.00
  • 10,000 iterations : 00:00:00.00
  • 100,000 iterations : 00:00:00.0312378

As you can see in the source code, I simply use a DateTime before and after the call, and everything runs on the UI thread.

And the source is stored on box.net for those that want a peek. Be aware it's a visual Studio 2008 solution and project.

Download the source.

Tuesday, 31 July 2007

More new WPF 3.5 things

Neil passed me some of these URLs in my comments and i'm all too happy to republish them.

Technorati Tags: , , , ,

Monday, 30 July 2007

WPF / Control interop, the blended way...

More of a bookmark for later, but seems Leslie and Jeremiah are doing fancy stuff with win32 controls and getting rid of the Hwnd limitation. Well done!

Edit: Leslie gave the link to the source code in the comments, so here it is. Thanks Leslie! http://www.codeplex.com/WPFWin32Renderer/Release

Technorati Tags: , , , , ,

A Bit of WPF love. No, Bits of it!

Tim Sneath posted an answer to the question I asked on friday (well, posted something that answered my question, probably without him being aware of it in the first place):

Most of the performance improvements and some of the feature improvements will also be included in a forthcoming service pack for .NET Framework 3.0 - I don't think we've talked externally about delivery mechanisms for this at this stage, however.

Good news! So I was right, there is a service pack in the pipeline!

Friday, 27 July 2007

So there *are* new things for WPF in 3.5

Visual Studio 2008 beta 2 has just been released, and with it an announcement of new features and bug fixes for WPF. And the best bit for me:

Data binding and journaling by URI work together.

For the WPF application we released to National Express with Netstore, this bug hit us very hard, as we designed a whole navigation system based on passing context / data objects around while keeping navigation by URL (memory footprint being the main reason why you want to navigate by URL.) What used to happen was that any Binding associated with a dependency property that supported Journaling would not be re-established. My good friend Neil Mosafi (seems to not have a blog anymore) was the one that found the bug.

Question for Microsoft though, is there going to be a HotFix for the issue? Some applications in the wild would definitely benefit from having a fix, rather than rely on inheriting every control and overriding each journaled dependency property manually.

As I'm writing this entry, my mind wanders and hope that one day WPF applications will work more like the browser, keeping a page as KeepAlive for a known number of pages or memory footprint, and only release a page when the history stack takes up too much memory. Maybe in .net 4 with WPF (which would be wpf 2.0, if 3.5 is WPF 1.5. Interesting arithmetic, WPF = .net - 2).

Oh the thrill. I'll be starting cooking some WPF examples next week as soon as I switch back to booting Vista instead of MacOS X (have a look at VMWare Fusion. Now if they could integrate their Unity feature with the vista DWM, I could get accelerated graphics WPF development in visual studio within macos. Hmmm...)

Technorati Tags: , ,