Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added .net client for signalR.

  • Loading branch information...
commit 147260a857605e82d9e3189c0850ea6f9d3d7680 1 parent 3b93a20
@davidfowl davidfowl authored
View
82 SignalR.Client/Connection.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using SignalR.Client.Transports;
+
+namespace SignalR.Client {
+ public class Connection {
+ public event Action<string> Received;
+ public event Func<string> Sending;
+ public event Action Closed;
+
+ private readonly IClientTransport _transport = new LongPollingTransport();
+
+ public Connection(string url) {
+ if (!url.StartsWith("/")) {
+ url += "/";
+ }
+
+ Url = url;
+ }
+
+ public string Url { get; set; }
+
+ internal long? MessageId { get; set; }
+
+ internal string ClientId { get; set; }
+
+ public bool IsActive { get; private set; }
+
+ public virtual Task Start() {
+ if (IsActive) {
+ return TaskAsyncHelper.Empty;
+ }
+
+ IsActive = true;
+
+ string data = String.Empty;
+ if (Sending != null) {
+ data = Sending();
+ }
+
+ string negotiateUrl = Url + "negotiate";
+
+ return HttpHelper.PostAsync(negotiateUrl).Success(task => {
+ string raw = task.Result.ReadAsString();
+
+ var negotiationResponse = JsonConvert.DeserializeObject<NegotiationResponse>(raw);
+
+ ClientId = negotiationResponse.ClientId;
+
+ _transport.Start(this, data);
+ });
+ }
+
+ public virtual void Stop() {
+ try {
+ _transport.Stop(this);
+
+ if (Closed != null) {
+ Closed();
+ }
+ }
+ finally {
+ IsActive = false;
+ }
+ }
+
+ public Task Send(string data) {
+ return Send<object>(data);
+ }
+
+ public Task<T> Send<T>(string data) {
+ return _transport.Send<T>(this, data);
+ }
+
+ internal void OnReceived(string message) {
+ if (Received != null) {
+ Received(message);
+ }
+ }
+ }
+}
View
19 SignalR.Client/Hubs/HubActionAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace SignalR.Client.Hubs {
+ [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
+ public sealed class HubActionAttribute : Attribute {
+ // See the attribute guidelines at
+ // http://go.microsoft.com/fwlink/?LinkId=85236
+ private readonly string _message;
+
+ // This is a positional argument
+ public HubActionAttribute(string message) {
+ _message = message;
+ }
+
+ public string Message {
+ get { return _message; }
+ }
+ }
+}
View
19 SignalR.Client/Hubs/HubAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace SignalR.Client.Hubs {
+ [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
+ public sealed class HubAttribute : Attribute {
+ // See the attribute guidelines at
+ // http://go.microsoft.com/fwlink/?LinkId=85236
+ private readonly string _type;
+
+ // This is a positional argument
+ public HubAttribute(string type) {
+ _type = type;
+ }
+
+ public string Type {
+ get { return _type; }
+ }
+ }
+}
View
152 SignalR.Client/Hubs/HubConnection.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+#if WINDOWS_PHONE
+using System.Windows;
+#endif
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace SignalR.Client.Hubs {
+ public class HubConnection : Connection {
+ private readonly Dictionary<string, Tuple<Type, MethodInfo>> _actionMap = new Dictionary<string, Tuple<Type, MethodInfo>>(StringComparer.OrdinalIgnoreCase);
+ private readonly Lazy<IEnumerable<string>> _actions;
+
+ public HubConnection(string baseUrl)
+ : base(baseUrl) {
+ _actions = new Lazy<IEnumerable<string>>(GetActions);
+ }
+
+ public Func<Type, object> ObjectFactory { get; set; }
+
+ public override Task Start() {
+ Sending += OnSending;
+ Received += OnReceived;
+ return base.Start();
+ }
+
+ public override void Stop() {
+ Sending -= OnSending;
+ Received -= OnReceived;
+ base.Stop();
+ }
+
+ public IHubProxy CreateProxy(string hub) {
+ return new HubProxy(this, hub);
+ }
+
+ private string OnSending() {
+ return JsonConvert.SerializeObject(_actions.Value);
+ }
+
+ private void OnReceived(string message) {
+ var info = JsonConvert.DeserializeObject<HubInvocationInfo>(message);
+ Tuple<Type, MethodInfo> mapping;
+ if (_actionMap.TryGetValue(info.Action, out mapping)) {
+ Type hubType = mapping.Item1;
+ MethodInfo method = mapping.Item2;
+
+ ObjectFactory = ObjectFactory ?? Activator.CreateInstance;
+
+ var hub = ObjectFactory(hubType);
+ method.Invoke(hub, ResolveParameters(method, info.Args));
+ }
+ }
+
+ private object[] ResolveParameters(MethodInfo method, object[] args) {
+ return (from p in method.GetParameters()
+ orderby p.Position
+ select ChangeType(args[p.Position], p.ParameterType)).ToArray();
+ }
+
+ private object ChangeType(object value, Type type) {
+ var jsonObject = value as JObject;
+ if (jsonObject != null) {
+ if (type == typeof(object)) {
+ return jsonObject;
+ }
+ else {
+ return JsonConvert.DeserializeObject(jsonObject.ToString(), type);
+ }
+ }
+ return Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
+ }
+
+
+ private IEnumerable<string> GetActions() {
+#if !WINDOWS_PHONE
+ foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) {
+#else
+ var parts = Deployment.Current.Parts;
+ foreach (var part in parts) {
+ var assemblyName = part.Source.Replace(".dll", String.Empty);
+ var a = Assembly.Load(assemblyName);
+#endif
+ foreach (var action in GetActions(a)) {
+ yield return action;
+ }
+ }
+ }
+
+ private IEnumerable<string> GetActions(Assembly assembly) {
+ foreach (var type in assembly.GetTypes()) {
+ var attr = (HubAttribute)type.GetCustomAttributes(typeof(HubAttribute), true).FirstOrDefault();
+ if (attr == null) {
+ continue;
+ }
+
+ foreach (var method in type.GetMethods()) {
+ string action = attr.Type + "." + GetMessage(method);
+ _actionMap[action] = Tuple.Create(type, method);
+ yield return action;
+ }
+ }
+ }
+
+ private string GetMessage(MethodInfo method) {
+ var attr = (HubActionAttribute)method.GetCustomAttributes(typeof(HubActionAttribute), true).FirstOrDefault();
+ if (attr == null) {
+ return method.Name;
+ }
+ return attr.Message;
+ }
+
+ public class HubInvocationInfo {
+ public string Action { get; set; }
+ public object[] Args { get; set; }
+ }
+
+#if WINDOWS_PHONE
+ private class Lazy<T> where T : class {
+ private readonly Func<T> _factory;
+ private T _value;
+ private readonly object _lock = new object();
+ public Lazy(Func<T> factory) {
+ _factory = factory;
+ }
+
+ public T Value {
+ get {
+ if (_value == null) {
+ lock (_lock) {
+ if (_value == null) {
+ _value = _factory();
+ }
+ }
+ }
+ return _value;
+ }
+ }
+ }
+#endif
+ private static string GetUrl(string baseUrl) {
+ if (!baseUrl.EndsWith("/")) {
+ baseUrl += "/";
+ }
+ return baseUrl + "signalr";
+ }
+ }
+}
View
97 SignalR.Client/Hubs/HubProxy.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+#if !WINDOWS_PHONE
+using System.Dynamic;
+#endif
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace SignalR.Client.Hubs {
+#if !WINDOWS_PHONE
+ public class HubProxy : DynamicObject, IHubProxy {
+#else
+ public class HubProxy : IHubProxy {
+#endif
+ private readonly string _hub;
+ private readonly Connection _client;
+ private readonly Dictionary<string, object> _state = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
+
+ public HubProxy(Connection client, string hub) {
+ _client = client;
+ _hub = hub;
+ }
+
+ public object this[string name] {
+ get {
+ object value;
+ _state.TryGetValue(name, out value);
+ return value;
+ }
+ set {
+ _state[name] = value;
+ }
+ }
+
+ public Task Invoke(string action, params object[] args) {
+ return Invoke<object>(action, args);
+ }
+
+ public Task<T> Invoke<T>(string action, params object[] args) {
+ var hubData = new HubData {
+ Hub = _hub,
+ Action = action,
+ Data = args,
+ State = _state
+ };
+
+ var value = JsonConvert.SerializeObject(hubData);
+
+ return _client.Send<HubResult<T>>(value).ContinueWith(task => {
+ if (task.Result != null) {
+
+ if (task.Result.Error != null) {
+ throw new InvalidOperationException(task.Result.Error);
+ }
+
+ HubResult<T> hubResult = task.Result;
+ foreach (var pair in hubResult.State) {
+ this[pair.Key] = pair.Value;
+ }
+
+ return hubResult.Result;
+ }
+ return default(T);
+ });
+ }
+
+#if !WINDOWS_PHONE
+ public override bool TrySetMember(SetMemberBinder binder, object value) {
+ _state[binder.Name] = value;
+ return true;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result) {
+ _state.TryGetValue(binder.Name, out result);
+ return true;
+ }
+
+ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {
+ result = Invoke(binder.Name, args);
+ return true;
+ }
+#endif
+
+ public class HubData {
+ public Dictionary<string, object> State { get; set; }
+ public object[] Data { get; set; }
+ public string Action { get; set; }
+ public string Hub { get; set; }
+ }
+
+ public class HubResult<T> {
+ public T Result { get; set; }
+ public string Error { get; set; }
+ public IDictionary<string, object> State { get; set; }
+ }
+ }
+}
View
7 SignalR.Client/Hubs/HubRequest.cs
@@ -0,0 +1,7 @@
+using System.Collections.Generic;
+
+namespace SignalR.Client.Hubs {
+ public class HubRequest {
+ public IEnumerable<string> Actions { get; set; }
+ }
+}
View
11 SignalR.Client/Hubs/IHubProxy.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace SignalR.Client.Hubs {
+ public interface IHubProxy {
+ object this[string name] { get; set; }
+
+ Task Invoke(string action, params object[] args);
+ Task<T> Invoke<T>(string action, params object[] args);
+ }
+}
View
15 SignalR.Client/Hubs/ProxyExtensions.cs
@@ -0,0 +1,15 @@
+using Newtonsoft.Json.Linq;
+using SignalR.Client;
+using Newtonsoft.Json;
+
+namespace SignalR.Client.Hubs {
+ public static class ProxyExtensions {
+ public static T GetValue<T>(this IHubProxy proxy, string name) {
+ object value = proxy[name];
+ if (value is JObject && typeof(T) != typeof(JObject)) {
+ return JsonConvert.DeserializeObject<T>(((JObject)value).ToString());
+ }
+ return (T)value;
+ }
+ }
+}
View
9 SignalR.Client/NegotiationResponse.cs
@@ -0,0 +1,9 @@
+using System.Diagnostics;
+
+namespace SignalR.Client {
+ [DebuggerDisplay("{ClientId} {Url}")]
+ public class NegotiationResponse {
+ public string ClientId { get; set; }
+ public string Url { get; set; }
+ }
+}
View
36 SignalR.Client/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SignalR.Client")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("SignalR.Client")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2011")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("41e53abd-f2b0-40ea-bb54-8d5f96582b66")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
View
79 SignalR.Client/SignalR.Client.csproj
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>8.0.30703</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{EB46B9C6-EE37-48F9-835E-E49580E40E0A}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>SignalR.Client</RootNamespace>
+ <AssemblyName>SignalR.Client</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Newtonsoft.Json">
+ <HintPath>..\packages\Newtonsoft.Json.4.0.2\lib\net40\Newtonsoft.Json.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="..\SignalR.ScaleOut\HttpHelper.cs">
+ <Link>Infrastructure\HttpHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\SignalR\TaskAsyncHelper.cs">
+ <Link>Infrastructure\TaskAsyncHelper.cs</Link>
+ </Compile>
+ <Compile Include="..\SignalR\TaskWrapperAsyncResult.cs">
+ <Link>Infrastructure\TaskWrapperAsyncResult.cs</Link>
+ </Compile>
+ <Compile Include="Connection.cs" />
+ <Compile Include="Hubs\HubActionAttribute.cs" />
+ <Compile Include="Hubs\HubAttribute.cs" />
+ <Compile Include="Hubs\HubConnection.cs" />
+ <Compile Include="Hubs\HubProxy.cs" />
+ <Compile Include="Hubs\HubRequest.cs" />
+ <Compile Include="Hubs\IHubProxy.cs" />
+ <Compile Include="NegotiationResponse.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Hubs\ProxyExtensions.cs" />
+ <Compile Include="Transports\IClientTransport.cs" />
+ <Compile Include="Transports\LongPollingTransport.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
View
9 SignalR.Client/Transports/IClientTransport.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace SignalR.Client.Transports {
+ public interface IClientTransport {
+ void Start(Connection connection, string data);
+ Task<T> Send<T>(Connection connection, string data);
+ void Stop(Connection connection);
+ }
+}
View
120 SignalR.Client/Transports/LongPollingTransport.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace SignalR.Client.Transports {
+ public class LongPollingTransport : IClientTransport {
+ public void Start(Connection connection, string data) {
+ string url = connection.Url;
+
+ if (connection.MessageId == null) {
+ url += "connect";
+ }
+
+ var parameters = new Dictionary<string, string> {
+ { "data", data },
+ { "messageId", Convert.ToString(connection.MessageId) },
+ { "clientId", connection.ClientId },
+ { "transport", "longPolling" }
+ };
+
+ HttpHelper.PostAsync(url, parameters).ContinueWith(task => {
+ try {
+ if (!task.IsFaulted) {
+ // Get the response
+ var raw = task.Result.ReadAsString();
+
+ if (!String.IsNullOrEmpty(raw)) {
+ ProcessResponse(connection, raw);
+ }
+ }
+ }
+ finally {
+ if (task.IsFaulted) {
+ // If we can recover from this exception then sleep for 2 seconds
+ if (CanRecover(task.Exception)) {
+ Thread.Sleep(2000);
+ }
+ else {
+ // If we couldn't recover then we need to stop the connection
+ connection.Stop();
+ }
+ }
+
+ // Only continue if the connection is still active
+ if (connection.IsActive) {
+ Start(connection, data);
+ }
+ }
+ });
+ }
+
+ public Task<T> Send<T>(Connection connection, string data) {
+ string url = connection.Url + "send";
+
+ var postData = new Dictionary<string, string> {
+ { "data", data },
+ { "clientId", connection.ClientId },
+ { "transport" , "longPolling" }
+ };
+
+ return HttpHelper.PostAsync(url, postData).Success(task => {
+ string raw = task.Result.ReadAsString();
+
+ if (String.IsNullOrEmpty(raw)) {
+ return default(T);
+ }
+
+ return JsonConvert.DeserializeObject<T>(raw);
+ });
+ }
+
+ public void Stop(Connection connection) {
+ }
+
+ private bool CanRecover(Exception exception) {
+ var webException = exception.GetBaseException() as WebException;
+ if (webException != null) {
+ var httpResponse = (HttpWebResponse)webException.Response;
+ if (httpResponse != null &&
+ httpResponse.StatusCode != HttpStatusCode.InternalServerError) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void ProcessResponse(Connection connection, string response) {
+ if (connection.MessageId == null) {
+ connection.MessageId = 0;
+ }
+
+ try {
+ JObject result = JObject.Parse(response);
+ JToken messages = result["Messages"];
+
+ if (messages != null) {
+ if (messages.HasValues) {
+ foreach (var message in messages.Children()) {
+ try {
+ connection.OnReceived(message.ToString());
+ }
+ catch (Exception ex) {
+ Debug.WriteLine("Failed to process message: {0}", ex);
+ }
+ }
+ }
+ connection.MessageId = result["MessageId"].Value<long>();
+ }
+ }
+ catch (Exception ex) {
+ Debug.WriteLine("Failed to response: {0}", ex);
+ }
+ }
+ }
+}
View
4 SignalR.Client/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="Newtonsoft.Json" version="4.0.2" />
+</packages>
View
2  SignalR.ScaleOut/HttpHelper.cs
@@ -5,7 +5,7 @@
using System.Text;
using System.Threading.Tasks;
-namespace SignalR.ScaleOut {
+namespace SignalR {
public static class HttpHelper {
public static Task<HttpWebResponse> GetAsync(this HttpWebRequest request) {
return Task.Factory.FromAsync<HttpWebResponse>(request.BeginGetResponse, iar => (HttpWebResponse)request.EndGetResponse(iar), null);
View
6 SignalR.sln
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalR", "SignalR\SignalR.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalR.ScaleOut", "SignalR.ScaleOut\SignalR.ScaleOut.csproj", "{32D16B36-970E-4CF2-B954-78CB1833CBC1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalR.Client", "SignalR.Client\SignalR.Client.csproj", "{EB46B9C6-EE37-48F9-835E-E49580E40E0A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -25,6 +27,10 @@ Global
{32D16B36-970E-4CF2-B954-78CB1833CBC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32D16B36-970E-4CF2-B954-78CB1833CBC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32D16B36-970E-4CF2-B954-78CB1833CBC1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB46B9C6-EE37-48F9-835E-E49580E40E0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB46B9C6-EE37-48F9-835E-E49580E40E0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB46B9C6-EE37-48F9-835E-E49580E40E0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB46B9C6-EE37-48F9-835E-E49580E40E0A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Please sign in to comment.
Something went wrong with that request. Please try again.