├── .gitattributes ├── .gitignore ├── Droid ├── Assets │ └── AboutAssets.txt ├── CustomRenderers │ └── MessageRenderer.cs ├── MainActivity.cs ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs ├── Resources │ ├── AboutResources.txt │ ├── Resource.designer.cs │ ├── drawable-hdpi │ │ └── icon.png │ ├── drawable-xhdpi │ │ └── icon.png │ ├── drawable-xxhdpi │ │ └── icon.png │ ├── drawable │ │ ├── bubble_green.9.png │ │ ├── bubble_yellow.9.png │ │ ├── empty_contact.jpg │ │ └── icon.png │ └── layout │ │ ├── image_item_opponent.axml │ │ ├── image_item_owner.axml │ │ ├── item_row.axml │ │ ├── message_item_opponent.axml │ │ └── message_item_owner.axml ├── WebSocketImplementation │ └── WebSocketImplementation.cs ├── XamarinChat.Droid.csproj ├── XamarinChat.Droid.csproj.bak ├── app.config └── packages.config ├── README.md ├── XamarinChat ├── Controls │ ├── ChatListView.cs │ └── MessageViewCell.cs ├── Models │ └── ChatMessage.cs ├── Pages │ ├── BasePage.cs │ ├── ChatPage.cs │ ├── ChatPage.xaml │ └── ChatPage.xaml.cs ├── Properties │ └── AssemblyInfo.cs ├── Services │ ├── ChatServices.cs │ └── IChatServices.cs ├── ViewModels │ ├── BaseViewModel.cs │ ├── ChatMessageViewModel.cs │ └── ChatViewModel.cs ├── WebSocketImplementation │ ├── ConnectionHolder.cs │ ├── IConnectionHolder.cs │ ├── IWebSocket.cs │ ├── WebSocketRequest.cs │ └── WebSocketTransportLayer.cs ├── XamarinChat.cs ├── XamarinChat.csproj ├── app.config └── packages.config ├── XamarinFormChatApp.sln └── iOS ├── AppDelegate.cs ├── CustomRenderers ├── BubbleCell.cs ├── ChatBubble.cs ├── ChatListViewRenderer.cs └── MessageRenderer.cs ├── Entitlements.plist ├── Info.plist ├── Main.cs ├── Resources ├── Images.xcassets │ └── AppIcons.appiconset │ │ └── Contents.json ├── LaunchScreen.xib ├── empty_contact.jpg ├── green.png ├── grey.png └── group.png ├── WebSocketImplementation └── WebSocketImplementation.cs ├── XamarinChat.iOS.csproj ├── app.config └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /Droid/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with your package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); 20 | -------------------------------------------------------------------------------- /Droid/CustomRenderers/MessageRenderer.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | using Android.App; 8 | using Android.Content; 9 | using Android.OS; 10 | using Android.Runtime; 11 | using Android.Views; 12 | using Android.Widget; 13 | using Xamarin.Forms; 14 | using XamarinChat; 15 | using XamarinChat.Droid; 16 | using Xamarin.Forms.Platform.Android; 17 | using Android.Graphics; 18 | using System.Net; 19 | using System.ComponentModel; 20 | 21 | [assembly: ExportRenderer(typeof(MessageViewCell), typeof(MessageRenderer))] 22 | namespace XamarinChat.Droid 23 | { 24 | public class MessageRenderer : ViewCellRenderer 25 | { 26 | protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, ViewGroup parent, Context context) 27 | { 28 | var inflatorservice = (LayoutInflater)Forms.Context.GetSystemService(Android.Content.Context.LayoutInflaterService); 29 | var dataContext = item.BindingContext as ChatMessageViewModel; 30 | 31 | var textMsgVm = dataContext as ChatMessageViewModel; 32 | if (textMsgVm != null) 33 | { 34 | if (!string.IsNullOrEmpty(textMsgVm.Image)) 35 | { 36 | var template = (LinearLayout)inflatorservice.Inflate(textMsgVm.IsMine ? Resource.Layout.image_item_owner : Resource.Layout.image_item_opponent, null, false); 37 | 38 | template.FindViewById(Resource.Id.nick).Text = textMsgVm.IsMine ? "Me:" : textMsgVm.Name + ":"; 39 | template.FindViewById(Resource.Id.image).SetImageBitmap(GetImageBitmapFromUrl(textMsgVm.Image)); 40 | return template; 41 | } 42 | else 43 | { 44 | var template = (LinearLayout)inflatorservice.Inflate(textMsgVm.IsMine ? Resource.Layout.message_item_owner : Resource.Layout.message_item_opponent, null, false); 45 | 46 | template.FindViewById(Resource.Id.nick).Text = textMsgVm.IsMine ? "Me:" : textMsgVm.Name + ":"; 47 | template.FindViewById(Resource.Id.message).Text = textMsgVm.Message; 48 | return template; 49 | } 50 | } 51 | 52 | return base.GetCellCore(item, convertView, parent, context); 53 | } 54 | 55 | private Bitmap GetImageBitmapFromUrl(string url) 56 | { 57 | Bitmap imageBitmap = null; 58 | using (var webClient = new WebClient()) 59 | { 60 | var imageBytes = webClient.DownloadData(url); 61 | if (imageBytes != null && imageBytes.Length > 0) 62 | { 63 | imageBitmap = BitmapFactory.DecodeByteArray(imageBytes, 0, imageBytes.Length); 64 | } 65 | } 66 | return imageBitmap; 67 | } 68 | 69 | 70 | protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e) 71 | { 72 | base.OnCellPropertyChanged(sender, e); 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Droid/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Android.App; 4 | using Android.Content; 5 | using Android.Content.PM; 6 | using Android.Runtime; 7 | using Android.Views; 8 | using Android.Widget; 9 | using Android.OS; 10 | 11 | namespace XamarinChat.Droid 12 | { 13 | [Activity (Label = "XamarinChat.Droid", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 14 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity 15 | { 16 | protected override void OnCreate (Bundle bundle) 17 | { 18 | base.OnCreate (bundle); 19 | 20 | global::Xamarin.Forms.Forms.Init (this, bundle); 21 | 22 | LoadApplication (new App ()); 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Droid/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Droid/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using Android.App; 4 | 5 | // Information about this assembly is defined by the following attributes. 6 | // Change them to the values specific to your project. 7 | 8 | [assembly: AssemblyTitle ("XamarinChat.Droid")] 9 | [assembly: AssemblyDescription ("")] 10 | [assembly: AssemblyConfiguration ("")] 11 | [assembly: AssemblyCompany ("")] 12 | [assembly: AssemblyProduct ("")] 13 | [assembly: AssemblyCopyright ("shaungrech")] 14 | [assembly: AssemblyTrademark ("")] 15 | [assembly: AssemblyCulture ("")] 16 | 17 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 18 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 19 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 20 | 21 | [assembly: AssemblyVersion ("1.0.0")] 22 | 23 | // The following attributes are used to specify the signing key for the assembly, 24 | // if desired. See the Mono documentation for more information about signing. 25 | 26 | //[assembly: AssemblyDelaySign(false)] 27 | //[assembly: AssemblyKeyFile("")] 28 | 29 | -------------------------------------------------------------------------------- /Droid/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.axml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable/ 12 | icon.png 13 | 14 | layout/ 15 | main.axml 16 | 17 | values/ 18 | strings.xml 19 | 20 | In order to get the build system to recognize Android resources, set the build action to 21 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 22 | instead operate on resource IDs. When you compile an Android application that uses resources, 23 | the build system will package the resources for distribution and generate a class called "R" 24 | (this is an Android convention) that contains the tokens for each one of the resources 25 | included. For example, for the above Resources layout, this is what the R class would expose: 26 | 27 | public class R { 28 | public class drawable { 29 | public const int icon = 0x123; 30 | } 31 | 32 | public class layout { 33 | public const int main = 0x456; 34 | } 35 | 36 | public class strings { 37 | public const int first_string = 0xabc; 38 | public const int second_string = 0xbcd; 39 | } 40 | } 41 | 42 | You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main 43 | to reference the layout/main.axml file, or R.strings.first_string to reference the first 44 | string in the dictionary file values/strings.xml. 45 | -------------------------------------------------------------------------------- /Droid/Resources/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /Droid/Resources/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /Droid/Resources/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /Droid/Resources/drawable/bubble_green.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable/bubble_green.9.png -------------------------------------------------------------------------------- /Droid/Resources/drawable/bubble_yellow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable/bubble_yellow.9.png -------------------------------------------------------------------------------- /Droid/Resources/drawable/empty_contact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable/empty_contact.jpg -------------------------------------------------------------------------------- /Droid/Resources/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/Droid/Resources/drawable/icon.png -------------------------------------------------------------------------------- /Droid/Resources/layout/image_item_opponent.axml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 13 | 18 | 28 | 35 | 36 | 41 | 42 | -------------------------------------------------------------------------------- /Droid/Resources/layout/image_item_owner.axml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 17 | 22 | 32 | 39 | 40 | 45 | 46 | -------------------------------------------------------------------------------- /Droid/Resources/layout/item_row.axml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 20 | 21 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /Droid/Resources/layout/message_item_opponent.axml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 13 | 18 | 28 | 35 | 36 | 45 | 46 | -------------------------------------------------------------------------------- /Droid/Resources/layout/message_item_owner.axml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 17 | 22 | 32 | 39 | 40 | 49 | 50 | -------------------------------------------------------------------------------- /Droid/WebSocketImplementation/WebSocketImplementation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Text; 7 | using System.Collections.Generic; 8 | using XamarinChat.WebSocketImplementation; 9 | using System.Net; 10 | 11 | [assembly: Xamarin.Forms.Dependency(typeof(WebSocketImplementation))] 12 | namespace XamarinChat.WebSocketImplementation 13 | { 14 | public class WebSocketImplementation : IWebSocket 15 | { 16 | public ClientWebSocket WebSocket { get; set; } 17 | public Queue QueueOfSends { get; set; } 18 | public CancellationToken DisconectToken { get; set; } 19 | public Action ResolveWebSocketResponse { get; set; } 20 | public IConnectionHolder Connection { get; set; } 21 | public CookieContainer CookiesContainer 22 | { get { return WebSocket.Options.Cookies; } set { WebSocket.Options.Cookies = value; } } 23 | public WebSocketImplementation() 24 | { 25 | WebSocket = new ClientWebSocket(); 26 | QueueOfSends = new Queue(); 27 | } 28 | public async Task ConnectAsync(Uri uri, CancellationToken _disconnectToken) 29 | { 30 | try 31 | { 32 | await Task.Factory.StartNew(async () => 33 | { 34 | var uniInfo = uri; 35 | var disconnectToken = _disconnectToken; 36 | while (true) 37 | { 38 | if (IsConnected) 39 | { 40 | await ReadMessage(); 41 | await SendMessage(); 42 | } 43 | else 44 | { 45 | await WebSocket.ConnectAsync(uniInfo, disconnectToken); 46 | } 47 | } 48 | }, _disconnectToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); 49 | } 50 | catch (Exception ex) 51 | { 52 | var mes = ex.Message; 53 | } 54 | } 55 | private async Task SendMessage() 56 | { 57 | try 58 | { 59 | do 60 | { 61 | if (WebSocket.State != WebSocketState.Open) 62 | { 63 | // Make this a faulted task and trigger the OnError even to maintain consistency with the HttpBasedTransports 64 | var ex = new InvalidOperationException("Error_DataCannotBeSentDuringWebSocketReconnect"); 65 | Connection.OnError(ex); 66 | return; 67 | } 68 | var message = ""; 69 | QueueOfSends.TryDequeue(out message); 70 | if (string.IsNullOrEmpty(message) && !CanSendMessage(message)) 71 | return; 72 | 73 | var byteMessage = Encoding.UTF8.GetBytes(message); 74 | var segmnet = new ArraySegment(byteMessage); 75 | 76 | await WebSocket.SendAsync(segmnet, WebSocketMessageType.Text, true, DisconectToken); 77 | } while (QueueOfSends.Count > 0); 78 | } 79 | catch (Exception ex) 80 | { 81 | var men = ex.Message; 82 | } 83 | } 84 | private async Task ReadMessage() 85 | { 86 | try 87 | { 88 | WebSocketReceiveResult result; 89 | string receivedMessage = ""; 90 | var message = new ArraySegment(new byte[4096]); 91 | do 92 | { 93 | result = await WebSocket.ReceiveAsync(message, DisconectToken); 94 | if (result.MessageType != WebSocketMessageType.Text) 95 | break; 96 | var messageBytes = message.Skip(message.Offset).Take(result.Count).ToArray(); 97 | receivedMessage += Encoding.UTF8.GetString(messageBytes); 98 | } 99 | while (!result.EndOfMessage); 100 | if (receivedMessage != "{}" && !string.IsNullOrEmpty(receivedMessage)) 101 | { 102 | ResolveWebSocketResponse.Invoke(receivedMessage); 103 | Console.WriteLine("Received: {0}", receivedMessage); 104 | } 105 | } 106 | catch (Exception ex) 107 | { 108 | var mes = ex.Message; 109 | } 110 | } 111 | public Task SetRequestHeader(string key, string value) 112 | { 113 | //Connection.Headers.Add(key, value); 114 | Connection.AddHeader(key, value); 115 | return Task.FromResult(0); 116 | } 117 | bool CanSendMessage(string message) 118 | { 119 | return IsConnected && !string.IsNullOrEmpty(message); 120 | } 121 | public bool IsConnected => WebSocket.State == WebSocketState.Open; 122 | public bool IsAborted => (WebSocket.State == WebSocketState.Aborted || WebSocket.State == WebSocketState.Closed); 123 | 124 | public Task SendMessageAsync(string message) 125 | { 126 | QueueOfSends.Enqueue(message); 127 | return Task.FromResult(0); 128 | } 129 | 130 | public void DisposeSocket() 131 | { 132 | //ResolveWebSocketResponse = null; 133 | } 134 | 135 | public void AddHeader(string key, string value) 136 | { 137 | WebSocket.Options.SetRequestHeader(key, value); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /Droid/XamarinChat.Droid.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211} 9 | Library 10 | XamarinChat.Droid 11 | Assets 12 | Resources 13 | Resource 14 | Resources\Resource.designer.cs 15 | True 16 | True 17 | XamarinChat.Droid 18 | Properties\AndroidManifest.xml 19 | v8.0 20 | 21 | 22 | 23 | 24 | true 25 | full 26 | false 27 | bin\Debug 28 | DEBUG; 29 | prompt 30 | 4 31 | None 32 | false 33 | 34 | 35 | full 36 | true 37 | bin\Release 38 | prompt 39 | 4 40 | false 41 | false 42 | 43 | 44 | 45 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\MonoAndroid10\FormsViewGroup.dll 46 | 47 | 48 | 49 | ..\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ..\packages\Xamarin.Android.Arch.Core.Common.1.0.0\lib\MonoAndroid80\Xamarin.Android.Arch.Core.Common.dll 59 | 60 | 61 | ..\packages\Xamarin.Android.Arch.Lifecycle.Common.1.0.1\lib\MonoAndroid80\Xamarin.Android.Arch.Lifecycle.Common.dll 62 | 63 | 64 | ..\packages\Xamarin.Android.Arch.Lifecycle.Runtime.1.0.0\lib\MonoAndroid80\Xamarin.Android.Arch.Lifecycle.Runtime.dll 65 | 66 | 67 | ..\packages\Xamarin.Android.Support.Animated.Vector.Drawable.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Animated.Vector.Drawable.dll 68 | 69 | 70 | ..\packages\Xamarin.Android.Support.Annotations.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Annotations.dll 71 | 72 | 73 | ..\packages\Xamarin.Android.Support.Compat.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Compat.dll 74 | 75 | 76 | ..\packages\Xamarin.Android.Support.Core.UI.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Core.UI.dll 77 | 78 | 79 | ..\packages\Xamarin.Android.Support.Core.Utils.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Core.Utils.dll 80 | 81 | 82 | ..\packages\Xamarin.Android.Support.Design.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Design.dll 83 | 84 | 85 | ..\packages\Xamarin.Android.Support.Fragment.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Fragment.dll 86 | 87 | 88 | ..\packages\Xamarin.Android.Support.Media.Compat.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Media.Compat.dll 89 | 90 | 91 | ..\packages\Xamarin.Android.Support.Transition.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Transition.dll 92 | 93 | 94 | ..\packages\Xamarin.Android.Support.v4.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v4.dll 95 | 96 | 97 | ..\packages\Xamarin.Android.Support.v7.AppCompat.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v7.AppCompat.dll 98 | 99 | 100 | ..\packages\Xamarin.Android.Support.v7.CardView.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v7.CardView.dll 101 | 102 | 103 | ..\packages\Xamarin.Android.Support.v7.MediaRouter.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v7.MediaRouter.dll 104 | 105 | 106 | ..\packages\Xamarin.Android.Support.v7.Palette.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v7.Palette.dll 107 | 108 | 109 | ..\packages\Xamarin.Android.Support.v7.RecyclerView.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.v7.RecyclerView.dll 110 | 111 | 112 | ..\packages\Xamarin.Android.Support.Vector.Drawable.26.1.0.1\lib\MonoAndroid80\Xamarin.Android.Support.Vector.Drawable.dll 113 | 114 | 115 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\MonoAndroid10\Xamarin.Forms.Core.dll 116 | 117 | 118 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\MonoAndroid10\Xamarin.Forms.Platform.dll 119 | 120 | 121 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll 122 | 123 | 124 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll 125 | 126 | 127 | 128 | 129 | {86E41C61-C0E7-4355-AAA4-22063088FEF2} 130 | XamarinChat 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Designer 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /Droid/XamarinChat.Droid.csproj.bak: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211} 8 | Library 9 | XamarinChat.Droid 10 | Assets 11 | Resources 12 | Resource 13 | Resources\Resource.designer.cs 14 | True 15 | True 16 | XamarinChat.Droid 17 | Properties\AndroidManifest.xml 18 | v6.0 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug 25 | DEBUG; 26 | prompt 27 | 4 28 | None 29 | false 30 | 31 | 32 | full 33 | true 34 | bin\Release 35 | prompt 36 | 4 37 | false 38 | false 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ..\packages\Xamarin.Android.Support.v4.21.0.3.0\lib\MonoAndroid10\Xamarin.Android.Support.v4.dll 47 | 48 | 49 | ..\packages\Xamarin.Forms.1.3.5.6335\lib\MonoAndroid10\Xamarin.Forms.Platform.Android.dll 50 | 51 | 52 | ..\packages\Xamarin.Forms.1.3.5.6335\lib\MonoAndroid10\FormsViewGroup.dll 53 | 54 | 55 | ..\packages\Xamarin.Forms.1.3.5.6335\lib\MonoAndroid10\Xamarin.Forms.Core.dll 56 | 57 | 58 | ..\packages\Xamarin.Forms.1.3.5.6335\lib\MonoAndroid10\Xamarin.Forms.Xaml.dll 59 | 60 | 61 | 62 | 63 | {86E41C61-C0E7-4355-AAA4-22063088FEF2} 64 | XamarinChat 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Droid/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Droid/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real-Time Chat App With Xamarin.Form And SignalR using WebSocket as transport method 2 | 3 | This project shows how to use WebSocket to connect to **.NET Framework SignalR Server** using **Xamarin.Form**. 4 | 5 | First Create the implementation on my PCL with these Classes and Interfaces: 6 | 7 | **IWebSocke:** This is the interface that abstract the websocket behaviors from my PCL and the element used for the dependency inversion between PCL and each platform. 8 | 9 | **IConnectionHolder:** this interface abstract the platform of several dependency needed only on PCL but not on platforms. 10 | 11 | **ConnectionHolder:** Is the concrete implementation of IConnectionHolder interface and it has all logic needed at each platform. This class in injected on the Dependency Service of Xamarin.Form. 12 | 13 | **WebSocketRequest:** This class is a concrete implementation of the interface IRequest, SignalR make a http negotiation before use any transport methodology and this object is the one used to establish the connection via HTTP for negotiation or for others transport method like Long Polling for example. 14 | 15 | **WebSocketTransportLayer:** This class is a concrete implementation of the interface IClientTransport which is the interface used by the SignalR client to make the connections using the best transport methods the clients supports. 16 | 17 | The second step was creating the websocket functionalities on each platform implementing the Interface that will be injected to abstract the platform behavior on the PCL. 18 | 19 | **WebSocketImplementation:** This object is implemented on each platform IOS and Android and it implement the IWebSocket interface and is injected on the Dependency Service on Xamarin.Form like this **[assembly: Xamarin.Forms.Dependency(typeof(WebSocketImplementation))]** . 20 | -------------------------------------------------------------------------------- /XamarinChat/Controls/ChatListView.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | namespace XamarinChat 4 | { 5 | public class ChatListView : ListView 6 | { 7 | 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /XamarinChat/Controls/MessageViewCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | 4 | namespace XamarinChat 5 | { 6 | public class MessageViewCell : ViewCell 7 | { 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /XamarinChat/Models/ChatMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XamarinChat 4 | { 5 | public class ChatMessage 6 | { 7 | public string Name { 8 | get; 9 | set; 10 | } 11 | 12 | public string Message { 13 | get; 14 | set; 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /XamarinChat/Pages/BasePage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | 4 | namespace XamarinChat 5 | { 6 | public abstract class BasePage : ContentPage where TViewModel : BaseViewModel 7 | { 8 | #region ViewModel and Binding Context 9 | 10 | /// 11 | /// Gets the generically typed ViewModel from the underlying BindingContext. 12 | /// 13 | /// The generically typed ViewModel. 14 | protected TViewModel ViewModel { 15 | get { return base.BindingContext as TViewModel; } 16 | } 17 | 18 | /// 19 | /// Sets the underlying BindingContext as the defined generic type. 20 | /// 21 | /// The generically typed ViewModel. 22 | /// Enforces a generically typed BindingContext, instead of the underlying loosely object-typed BindingContext. 23 | public new TViewModel BindingContext { 24 | set { 25 | base.BindingContext = value; 26 | base.OnPropertyChanged ("BindingContext"); 27 | } 28 | } 29 | 30 | #endregion 31 | 32 | protected override void OnAppearing () 33 | { 34 | base.OnAppearing (); 35 | BackgroundColor = Color.White; 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /XamarinChat/Pages/ChatPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | 4 | namespace XamarinChat 5 | { 6 | public class ChatPage : BasePage 7 | { 8 | private Entry nameEntry; 9 | private Entry messageEntry; 10 | private Button sendMessageButton; 11 | private Button joinButton; 12 | 13 | public ChatPage () 14 | { 15 | //Join Room Button 16 | joinButton = new Button { 17 | Text="Join Group" 18 | }; 19 | joinButton.SetBinding (Button.CommandProperty, "JoinRoomCommand"); 20 | 21 | //Init Name Entry 22 | nameEntry = new Entry { 23 | TextColor = Color.Black 24 | }; 25 | nameEntry.SetBinding (Entry.TextProperty, "ChatMessage.Name", BindingMode.TwoWay); 26 | nameEntry.Text = "BaseRoom"; 27 | //Init Message Entry 28 | messageEntry = new Entry { 29 | TextColor = Color.Black 30 | }; 31 | messageEntry.SetBinding (Entry.TextProperty, "ChatMessage.Message"); 32 | messageEntry.Text = "Write your message"; 33 | sendMessageButton = new Button { 34 | Text = "Send Message" 35 | }; 36 | sendMessageButton.SetBinding (Button.CommandProperty, "SendMessageCommand"); 37 | 38 | var messageList = new ChatListView(); 39 | messageList.VerticalOptions = LayoutOptions.FillAndExpand; 40 | messageList.SetBinding(ChatListView.ItemsSourceProperty, new Binding("Messages")); 41 | messageList.ItemTemplate = new DataTemplate(CreateMessageCell); 42 | 43 | var stackLayout = new StackLayout () { 44 | Orientation = StackOrientation.Vertical, 45 | Padding = new Thickness (5, 20, 5, 10), 46 | Children = { 47 | joinButton, 48 | nameEntry, 49 | messageEntry, 50 | sendMessageButton 51 | } 52 | }; 53 | 54 | Content = new StackLayout 55 | { 56 | Children= 57 | { 58 | stackLayout, 59 | messageList 60 | } 61 | }; 62 | } 63 | 64 | private Cell CreateMessageCell() 65 | { 66 | var timestampLabel = new Label(); 67 | timestampLabel.SetBinding(Label.TextProperty, new Binding("Timestamp", stringFormat: "[{0:HH:mm}]")); 68 | timestampLabel.TextColor = Color.Silver; 69 | timestampLabel.FontSize = 14; 70 | 71 | var authorLabel = new Label(); 72 | authorLabel.SetBinding(Label.TextProperty, new Binding("ChatMessage.Name", stringFormat: "{0}: ")); 73 | authorLabel.TextColor = Device.OnPlatform(Color.Blue, Color.Yellow, Color.Yellow); 74 | authorLabel.FontSize = 14; 75 | 76 | var messageLabel = new Label(); 77 | messageLabel.SetBinding(Label.TextProperty, new Binding("ChatMessage.Message")); 78 | messageLabel.FontSize = 14; 79 | 80 | var stack = new StackLayout 81 | { 82 | Orientation = StackOrientation.Horizontal, 83 | Children = {authorLabel, messageLabel} 84 | }; 85 | 86 | if (Device.Idiom == TargetIdiom.Tablet) 87 | { 88 | stack.Children.Insert(0, timestampLabel); 89 | } 90 | 91 | var view = new MessageViewCell 92 | { 93 | View = stack 94 | }; 95 | return view; 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /XamarinChat/Pages/ChatPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /XamarinChat/Pages/ChatPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using Xamarin.Forms; 5 | 6 | namespace XamarinChat 7 | { 8 | public partial class ChatPage : BasePage 9 | { 10 | public ChatPage () 11 | { 12 | InitializeComponent (); 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /XamarinChat/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | 7 | [assembly: AssemblyTitle ("XamarinChat")] 8 | [assembly: AssemblyDescription ("")] 9 | [assembly: AssemblyConfiguration ("")] 10 | [assembly: AssemblyCompany ("")] 11 | [assembly: AssemblyProduct ("")] 12 | [assembly: AssemblyCopyright ("shaungrech")] 13 | [assembly: AssemblyTrademark ("")] 14 | [assembly: AssemblyCulture ("")] 15 | 16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 19 | 20 | [assembly: AssemblyVersion ("1.0.*")] 21 | 22 | // The following attributes are used to specify the signing key for the assembly, 23 | // if desired. See the Mono documentation for more information about signing. 24 | 25 | //[assembly: AssemblyDelaySign(false)] 26 | //[assembly: AssemblyKeyFile("")] 27 | 28 | -------------------------------------------------------------------------------- /XamarinChat/Services/ChatServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNet.SignalR.Client; 3 | using Xamarin.Forms; 4 | using XamarinChat; 5 | using System.Threading.Tasks; 6 | using System.Collections.Generic; 7 | using Microsoft.AspNet.SignalR.Client.Transports; 8 | using XamarinChat.WebSocketImplementation; 9 | 10 | [assembly: Dependency (typeof(ChatServices))] 11 | namespace XamarinChat 12 | { 13 | public class ChatServices : IChatServices 14 | { 15 | private readonly HubConnection _connection; 16 | private readonly IHubProxy _proxy; 17 | 18 | public event EventHandler OnMessageReceived; 19 | 20 | public ChatServices () 21 | { 22 | _connection = new HubConnection (App.SignalRUrl); 23 | _proxy = _connection.CreateHubProxy ("chatGroupHub"); 24 | _proxy.On("GetMessage", (string name, string message) => OnMessageReceived(this, new ChatMessage 25 | { 26 | Name = name, 27 | Message = message 28 | })); 29 | } 30 | 31 | #region IChatServices implementation 32 | 33 | public async Task Connect () 34 | { 35 | 36 | var http = new Microsoft.AspNet.SignalR.Client.Http.DefaultHttpClient(); 37 | //var transports = new List() 38 | // { 39 | // new WebSocketTransportLayer(http), 40 | // new ServerSentEventsTransport(http), 41 | // new LongPollingTransport(http) 42 | // }; 43 | /// Preparando la conexion 44 | //await _connection.Start(new AutoTransport(http, transports)); 45 | await _connection.Start(new WebSocketTransportLayer(http)); 46 | } 47 | 48 | public async Task Send (ChatMessage message, string roomName) 49 | { 50 | if (_connection.State == ConnectionState.Disconnected) 51 | { 52 | await Connect(); 53 | } 54 | await _proxy.Invoke ("SendMessage", message.Name, message.Message, roomName); 55 | } 56 | 57 | public async Task JoinGroup(string roomName) 58 | { 59 | if (_connection.State == ConnectionState.Disconnected) 60 | { 61 | await Connect(); 62 | } 63 | await _proxy.Invoke ("JoinGroup", roomName); 64 | } 65 | 66 | #endregion 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /XamarinChat/Services/IChatServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace XamarinChat 5 | { 6 | public interface IChatServices 7 | { 8 | Task Connect(); 9 | Task Send(ChatMessage message, string roomName); 10 | Task JoinGroup(string roomName); 11 | event EventHandler OnMessageReceived; 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /XamarinChat/ViewModels/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Xamarin.Forms; 4 | using System.Collections.Generic; 5 | 6 | namespace XamarinChat 7 | { 8 | public class BaseViewModel : INotifyPropertyChanged 9 | { 10 | public bool IsInitialized { get; set; } 11 | 12 | bool canLoadMore; 13 | /// 14 | /// Gets or sets the "IsBusy" property 15 | /// 16 | /// The isbusy property. 17 | public const string CanLoadMorePropertyName = "CanLoadMore"; 18 | 19 | public bool CanLoadMore { 20 | get { return canLoadMore; } 21 | set { SetProperty (ref canLoadMore, value, CanLoadMorePropertyName); } 22 | } 23 | 24 | bool isBusy; 25 | /// 26 | /// Gets or sets the "IsBusy" property 27 | /// 28 | /// The isbusy property. 29 | public const string IsBusyPropertyName = "IsBusy"; 30 | 31 | public bool IsBusy { 32 | get { return isBusy; } 33 | set { SetProperty (ref isBusy, value, IsBusyPropertyName); } 34 | } 35 | 36 | bool isValid; 37 | /// 38 | /// Gets or sets the "IsValid" property 39 | /// 40 | /// The isbusy property. 41 | public const string IsValidPropertyName = "IsValid"; 42 | 43 | public bool IsValid { 44 | get { return isValid; } 45 | set { SetProperty (ref isValid, value, IsValidPropertyName); } 46 | } 47 | 48 | string subTitle = string.Empty; 49 | /// 50 | /// Gets or sets the "Subtitle" property 51 | /// 52 | public const string SubtitlePropertyName = "Subtitle"; 53 | 54 | public string Subtitle { 55 | get { return subTitle; } 56 | set { SetProperty (ref subTitle, value, SubtitlePropertyName); } 57 | } 58 | 59 | string icon = null; 60 | /// 61 | /// Gets or sets the "Icon" of the viewmodel 62 | /// 63 | public const string IconPropertyName = "Icon"; 64 | 65 | public string Icon { 66 | get { return icon; } 67 | set { SetProperty (ref icon, value, IconPropertyName); } 68 | } 69 | 70 | protected void SetProperty ( 71 | ref U backingStore, U value, 72 | string propertyName, 73 | Action onChanged = null, 74 | Action onChanging = null) 75 | { 76 | if (EqualityComparer.Default.Equals (backingStore, value)) 77 | return; 78 | 79 | if (onChanging != null) 80 | onChanging (value); 81 | 82 | OnPropertyChanging (propertyName); 83 | 84 | backingStore = value; 85 | 86 | if (onChanged != null) 87 | onChanged (); 88 | 89 | OnPropertyChanged (propertyName); 90 | } 91 | 92 | #region INotifyPropertyChanging implementation 93 | 94 | public event PropertyChangingEventHandler PropertyChanging; 95 | 96 | #endregion 97 | 98 | public void OnPropertyChanging (string propertyName) 99 | { 100 | if (PropertyChanging == null) 101 | return; 102 | 103 | PropertyChanging (this, new PropertyChangingEventArgs (propertyName)); 104 | } 105 | 106 | #region INotifyPropertyChanged implementation 107 | 108 | public event PropertyChangedEventHandler PropertyChanged; 109 | 110 | #endregion 111 | 112 | public void OnPropertyChanged (string propertyName) 113 | { 114 | if (PropertyChanged == null) 115 | return; 116 | 117 | PropertyChanged (this, new PropertyChangedEventArgs (propertyName)); 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /XamarinChat/ViewModels/ChatMessageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XamarinChat 4 | { 5 | public class ChatMessageViewModel:BaseViewModel 6 | { 7 | private string _name; 8 | 9 | public string Name { 10 | get{ return _name; } 11 | set { 12 | _name = value; 13 | OnPropertyChanged ("Name"); 14 | 15 | } 16 | } 17 | 18 | private string _message; 19 | 20 | public string Message { 21 | get{ return _message; } 22 | set { 23 | _message = value; 24 | OnPropertyChanged ("Message"); 25 | } 26 | } 27 | 28 | private string _image; 29 | 30 | public string Image { 31 | get{ return _image; } 32 | set { 33 | _image = value; 34 | OnPropertyChanged ("Image"); 35 | } 36 | } 37 | 38 | private bool _isMine; 39 | 40 | public bool IsMine { 41 | get{ return _isMine; } 42 | set { 43 | _isMine = value; 44 | OnPropertyChanged ("IsMine"); 45 | } 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /XamarinChat/ViewModels/ChatViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Xamarin.Forms; 3 | 4 | namespace XamarinChat 5 | { 6 | public class ChatViewModel : BaseViewModel 7 | { 8 | private IChatServices _chatServices; 9 | private string _roomName = "PrivateRoom"; 10 | 11 | 12 | #region ViewModel Properties 13 | 14 | private ObservableCollection _messages; 15 | 16 | public ObservableCollection Messages { 17 | get { return _messages; } 18 | set { 19 | _messages = value; 20 | OnPropertyChanged ("Messages"); 21 | } 22 | } 23 | 24 | private ChatMessageViewModel _chatMessage; 25 | public ChatMessageViewModel ChatMessage { 26 | get{ return _chatMessage; } 27 | set { 28 | _chatMessage = value; 29 | OnPropertyChanged ("ChatMessage"); 30 | } 31 | } 32 | 33 | 34 | #endregion 35 | 36 | public ChatViewModel () 37 | { 38 | _chatServices = DependencyService.Get (); 39 | _chatMessage = new ChatMessageViewModel (); 40 | 41 | _messages = new ObservableCollection (); 42 | _chatServices.Connect (); 43 | _chatServices.OnMessageReceived += _chatServices_OnMessageReceived; 44 | } 45 | 46 | void _chatServices_OnMessageReceived (object sender, ChatMessage e) 47 | { 48 | _messages.Add (new ChatMessageViewModel{ Name=e.Name,Message=e.Message,IsMine=_messages.Count%2==0 }); 49 | } 50 | 51 | #region Send Message Command 52 | 53 | Command sendMessageCommand; 54 | 55 | /// 56 | /// Command to Send Message 57 | /// 58 | public Command SendMessageCommand { 59 | get { 60 | return sendMessageCommand ?? 61 | (sendMessageCommand = new Command (ExecuteSendMessageCommand)); 62 | } 63 | } 64 | 65 | async void ExecuteSendMessageCommand () 66 | { 67 | IsBusy = true; 68 | await _chatServices.Send (new ChatMessage{ Name = _chatMessage.Name, Message = _chatMessage.Message },_roomName); 69 | IsBusy = false; 70 | } 71 | 72 | #endregion 73 | 74 | #region Join Room Command 75 | 76 | Command joinRoomCommand; 77 | 78 | /// 79 | /// Command to Send Message 80 | /// 81 | public Command JoinRoomCommand { 82 | get { 83 | return joinRoomCommand ?? 84 | (joinRoomCommand = new Command (ExecuteJoinRoomCommand)); 85 | } 86 | } 87 | 88 | async void ExecuteJoinRoomCommand () 89 | { 90 | IsBusy = true; 91 | await _chatServices.JoinGroup(_roomName); 92 | IsBusy = false; 93 | } 94 | 95 | #endregion 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /XamarinChat/WebSocketImplementation/ConnectionHolder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNet.SignalR.Client; 2 | using System; 3 | 4 | namespace XamarinChat.WebSocketImplementation 5 | { 6 | public class ConnectionHolder : IConnectionHolder 7 | { 8 | private IConnection _connection; 9 | public ConnectionHolder(IConnection connection) 10 | { 11 | this._connection = connection; 12 | } 13 | public void AddHeader(string key, string value) 14 | { 15 | _connection.Headers.Add(key, value); 16 | } 17 | 18 | public void OnError(Exception ex) 19 | { 20 | _connection.OnError(ex); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XamarinChat/WebSocketImplementation/IConnectionHolder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XamarinChat.WebSocketImplementation 4 | { 5 | public interface IConnectionHolder 6 | { 7 | void OnError(Exception ex); 8 | void AddHeader(string key, string value); 9 | } 10 | } -------------------------------------------------------------------------------- /XamarinChat/WebSocketImplementation/IWebSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | 7 | namespace XamarinChat.WebSocketImplementation 8 | { 9 | public interface IWebSocket 10 | { 11 | bool IsConnected { get; } 12 | Queue QueueOfSends { get; set; } 13 | CancellationToken DisconectToken { get; set; } 14 | Action ResolveWebSocketResponse { get; set; } 15 | IConnectionHolder Connection { get; set; } 16 | Task SetRequestHeader(string Key, string Value); 17 | Task ConnectAsync(Uri uri, CancellationToken _disconnectToken); 18 | Task SendMessageAsync(string message); 19 | void DisposeSocket(); 20 | void AddHeader(string key, string value); 21 | CookieContainer CookiesContainer { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /XamarinChat/WebSocketImplementation/WebSocketRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNet.SignalR.Client.Http; 3 | using System.Net; 4 | using Microsoft.AspNet.SignalR.Client; 5 | 6 | namespace XamarinChat.WebSocketImplementation 7 | { 8 | internal class WebSocketRequest : IRequest 9 | { 10 | private readonly IWebSocket _webSocket; 11 | private readonly IConnection Connection; 12 | public WebSocketRequest(IWebSocket webSocket, IConnection connection) 13 | { 14 | _webSocket = webSocket; 15 | this.Connection = connection; 16 | PrepareRequest(); 17 | } 18 | 19 | public string UserAgent 20 | { 21 | get; 22 | set; 23 | } 24 | 25 | public string Accept 26 | { 27 | get; 28 | set; 29 | } 30 | 31 | public void Abort() 32 | { 33 | } 34 | public CookieContainer CookieContainer 35 | { 36 | get { return _webSocket.CookiesContainer; } 37 | set { _webSocket.CookiesContainer = value; } 38 | } 39 | public void SetRequestHeaders(IDictionary headers) 40 | { 41 | if (headers == null) 42 | { 43 | throw new System.Exception("Header Empty"); 44 | } 45 | foreach (var header in headers) 46 | { 47 | _webSocket.AddHeader(header.Key, header.Value); 48 | } 49 | } 50 | private void PrepareRequest() 51 | { 52 | if (Connection.CookieContainer != null) 53 | { 54 | CookieContainer = Connection.CookieContainer; 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /XamarinChat/WebSocketImplementation/WebSocketTransportLayer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNet.SignalR.Client.Transports; 2 | using System; 3 | using Microsoft.AspNet.SignalR.Client; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNet.SignalR.Client.Http; 7 | using Microsoft.AspNet.SignalR.Client.Infrastructure; 8 | using Newtonsoft.Json.Linq; 9 | using System.Collections.Generic; 10 | using System.Globalization; 11 | using Newtonsoft.Json; 12 | using Xamarin.Forms; 13 | 14 | namespace XamarinChat.WebSocketImplementation 15 | { 16 | public class WebSocketTransportLayer : IClientTransport 17 | { 18 | private IWebSocket _webSocket; 19 | private const ushort SuccessCloseStatus = 1000; 20 | private IConnection _connection; 21 | private string _connectionData; 22 | private CancellationToken _disconnectToken; 23 | private CancellationTokenSource _disconnectTokenSource; 24 | private bool _finished = false; 25 | private IHttpClient httpClient; 26 | private readonly TransportAbortHandler _abortHandler; 27 | 28 | public WebSocketTransportLayer(IHttpClient httpClient) 29 | { 30 | this.httpClient = httpClient; 31 | _abortHandler = new TransportAbortHandler(httpClient, Name); 32 | ReconnectDelay = TimeSpan.FromSeconds(5); 33 | } 34 | /// 35 | /// The time to wait after a connection drops to try reconnecting. 36 | /// 37 | public TimeSpan ReconnectDelay { get; set; } 38 | 39 | public bool SupportsKeepAlive 40 | { 41 | get { return true; } 42 | } 43 | 44 | public string Name { get { return "webSockets"; } } 45 | 46 | 47 | protected internal void TransportFailed(Exception ex) 48 | { 49 | // will be no-op if handler already finished (either succeeded or failed) 50 | } 51 | public async Task Start(IConnection connection, string connectionData, CancellationToken disconnectToken) 52 | { 53 | _connection = connection; 54 | _connectionData = connectionData; 55 | _disconnectToken = disconnectToken; 56 | await Start(connection, connectionData); 57 | } 58 | private async Task Start(IConnection connection, string connectionData) 59 | { 60 | try 61 | { 62 | await StartWebSocket(connection, UrlBuilder.BuildConnect(connection, Name, connectionData)); 63 | } 64 | catch (TaskCanceledException) 65 | { 66 | TransportFailed(null); 67 | } 68 | catch (Exception ex) 69 | { 70 | TransportFailed(ex); 71 | } 72 | } 73 | 74 | protected void OnStartFailed() 75 | { 76 | // if the transport failed to start we want to stop it silently. 77 | Dispose(); 78 | } 79 | 80 | private async Task StartWebSocket(IConnection connection, string url) 81 | { 82 | var uri = UrlBuilder.ConvertToWebSocketUri(url); 83 | connection.Trace(TraceLevels.Events, "WS Connecting to: {0}", uri); 84 | 85 | if (_webSocket == null) 86 | { 87 | var holder = new ConnectionHolder(connection); 88 | 89 | _webSocket = DependencyService.Get(); 90 | _webSocket.ResolveWebSocketResponse = MessageReceived; 91 | if (_disconnectTokenSource == null) 92 | { 93 | _disconnectTokenSource = new CancellationTokenSource(); 94 | } 95 | _webSocket.DisconectToken = _disconnectTokenSource.Token;//_disconnectTokenSource.Token; 96 | _webSocket.Connection = holder; 97 | connection.PrepareRequest(new WebSocketRequest(_webSocket, connection)); 98 | await OpenWebSocket(_webSocket, uri); 99 | } 100 | } 101 | 102 | // testing/mocking 103 | protected virtual async Task OpenWebSocket(IWebSocket webSocket, Uri uri) 104 | { 105 | CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_disconnectTokenSource.Token, _disconnectToken); 106 | CancellationToken token = linkedCts.Token; 107 | await webSocket.ConnectAsync(uri, token); 108 | await Task.Delay(ReconnectDelay); 109 | } 110 | internal void MessageReceived(string response) 111 | { 112 | _connection.Trace(TraceLevels.Messages, "WS: OnMessage({0})", response); 113 | 114 | ProcessResponse(_connection, response); 115 | } 116 | 117 | public Task Send(IConnection connection, string data, string connectionData) 118 | { 119 | if (connection == null) 120 | { 121 | throw new ArgumentNullException("connection"); 122 | } 123 | 124 | var webSocket = _webSocket; 125 | 126 | if (webSocket == null) 127 | { 128 | Exception ex; 129 | if (connection.State != ConnectionState.Disconnected) 130 | { 131 | // Make this a faulted task and trigger the OnError even to maintain consistency with the HttpBasedTransports 132 | ex = new InvalidOperationException("Error Data Cannot Be Sent During Web Socket Reconnect"); 133 | connection.OnError(ex); 134 | } 135 | else 136 | { 137 | ex = new InvalidOperationException("Data Cannot Be Sent During WebSocket Reconnect"); 138 | } 139 | 140 | var tcs = new TaskCompletionSource(); 141 | tcs.SetException(ex); 142 | return tcs.Task; 143 | } 144 | 145 | return Send(webSocket, data); 146 | } 147 | 148 | // internal for testing 149 | internal static async Task Send(IWebSocket webSocket, string data) 150 | { 151 | await webSocket.SendMessageAsync(data); 152 | } 153 | 154 | // internal for testing 155 | internal async Task Reconnect(IConnection connection, string connectionData) 156 | { 157 | var reconnectUrl = UrlBuilder.BuildReconnect(connection, Name, connectionData); 158 | if (ConnectionState.Disconnected == connection.State || !_webSocket.IsConnected) 159 | { 160 | while (TransportHelper.VerifyLastActive(connection) && connection.EnsureReconnecting() && !_disconnectToken.IsCancellationRequested) 161 | { 162 | try 163 | { 164 | await StartWebSocket(connection, reconnectUrl); 165 | 166 | if (connection.ChangeState(ConnectionState.Reconnecting, ConnectionState.Connected)) 167 | { 168 | connection.OnReconnected(); 169 | } 170 | 171 | break; 172 | } 173 | catch (OperationCanceledException) 174 | { 175 | break; 176 | } 177 | catch (Exception ex) 178 | { 179 | connection.OnError(ex); 180 | } 181 | await Task.Delay(ReconnectDelay); 182 | } 183 | } 184 | } 185 | 186 | public void LostConnection(IConnection connection) 187 | { 188 | if (connection == null) 189 | { 190 | throw new ArgumentNullException("connection"); 191 | } 192 | 193 | connection.Trace(TraceLevels.Events, "WS: LostConnection"); 194 | Task.Run(() => Reconnect(_connection, _connectionData)); 195 | } 196 | 197 | private void DisposeSocket() 198 | { 199 | _finished = true; 200 | _disconnectTokenSource.Cancel(); 201 | _disconnectTokenSource = null; 202 | var webSocket = Interlocked.Exchange(ref _webSocket, null); 203 | if (webSocket != null) 204 | { 205 | webSocket.DisposeSocket(); 206 | webSocket = null; 207 | } 208 | } 209 | 210 | public async Task Negotiate(IConnection connection, string connectionData) 211 | { 212 | if (_finished) 213 | { 214 | throw new InvalidOperationException("Error_TransportCannotBeReused"); 215 | } 216 | if (httpClient == null) 217 | { 218 | throw new ArgumentNullException("httpClient"); 219 | } 220 | 221 | if (connection == null) 222 | { 223 | throw new ArgumentNullException("connection"); 224 | } 225 | 226 | var negotiateUrl = UrlBuilder.BuildNegotiate(connection, connectionData); 227 | 228 | httpClient.Initialize(connection); 229 | var response = await httpClient.Get(negotiateUrl, connection.PrepareRequest, isLongRunning: false); 230 | if (response == null) 231 | { 232 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 233 | } 234 | else 235 | { 236 | var result = await response.ReadAsString(); 237 | if (string.IsNullOrEmpty(result)) 238 | { 239 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 240 | } 241 | 242 | return JsonConvert.DeserializeObject(result); 243 | } 244 | } 245 | 246 | public void Abort(IConnection connection, TimeSpan timeout, string connectionData) 247 | { 248 | _finished = true; 249 | // DisposeSocket(); 250 | _abortHandler.Abort(connection, timeout, connectionData); 251 | } 252 | public virtual async Task GetNegotiationResponse(IHttpClient httpClient, IConnection connection, string connectionData) 253 | { 254 | if (httpClient == null) 255 | { 256 | throw new ArgumentNullException("httpClient"); 257 | } 258 | 259 | if (connection == null) 260 | { 261 | throw new ArgumentNullException("connection"); 262 | } 263 | 264 | var negotiateUrl = UrlBuilder.BuildNegotiate(connection, connectionData); 265 | 266 | httpClient.Initialize(connection); 267 | var response = await httpClient.Get(negotiateUrl, connection.PrepareRequest, isLongRunning: false); 268 | if (response == null) 269 | { 270 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 271 | } 272 | else 273 | { 274 | var result = await response.ReadAsString(); 275 | if (string.IsNullOrEmpty(result)) 276 | { 277 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 278 | } 279 | 280 | return JsonConvert.DeserializeObject(result); 281 | } 282 | } 283 | 284 | // virtual to allow mocking 285 | public virtual async Task GetStartResponse(IHttpClient httpClient, IConnection connection, string connectionData, string transport) 286 | { 287 | if (httpClient == null) 288 | { 289 | throw new ArgumentNullException("httpClient"); 290 | } 291 | 292 | if (connection == null) 293 | { 294 | throw new ArgumentNullException("connection"); 295 | } 296 | 297 | var startUrl = UrlBuilder.BuildStart(connection, transport, connectionData); 298 | var response = await httpClient.Get(startUrl, connection.PrepareRequest, isLongRunning: false); 299 | if (response == null) 300 | { 301 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 302 | } 303 | else 304 | { 305 | var result = await response.ReadAsString(); 306 | if (string.IsNullOrEmpty(result)) 307 | { 308 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 309 | } 310 | 311 | return result; 312 | } 313 | } 314 | 315 | 316 | public static bool VerifyLastActive(IConnection connection) 317 | { 318 | if (connection == null) 319 | { 320 | throw new ArgumentNullException("connection"); 321 | } 322 | // Ensure that we have not exceeded the reconnect window 323 | if (DateTime.UtcNow - connection.LastActiveAt >= connection.ReconnectWindow) 324 | { 325 | connection.Trace(TraceLevels.Events, "There has not been an active server connection for an extended period of time. Stopping connection."); 326 | connection.Stop(new TimeoutException(String.Format(CultureInfo.CurrentCulture, "Error_ReconnectWindowTimeout", 327 | connection.LastActiveAt, connection.ReconnectWindow))); 328 | return false; 329 | } 330 | return true; 331 | } 332 | 333 | protected internal virtual bool ProcessResponse(IConnection connection, string response) 334 | { 335 | if (connection == null) 336 | { 337 | throw new ArgumentNullException("connection"); 338 | } 339 | connection.MarkLastMessage(); 340 | if (String.IsNullOrEmpty(response)) 341 | { 342 | return false; 343 | } 344 | 345 | var shouldReconnect = false; 346 | 347 | try 348 | { 349 | var result = connection.JsonDeserializeObject(response); 350 | 351 | if (!result.HasValues) 352 | { 353 | return false; 354 | } 355 | 356 | if (result["I"] != null) 357 | { 358 | connection.OnReceived(result); 359 | return false; 360 | } 361 | 362 | shouldReconnect = (int?)result["T"] == 1; 363 | 364 | var groupsToken = result["G"]; 365 | if (groupsToken != null) 366 | { 367 | connection.GroupsToken = (string)groupsToken; 368 | } 369 | 370 | var messages = result["M"] as JArray; 371 | if (messages != null) 372 | { 373 | connection.MessageId = (string)result["C"]; 374 | 375 | foreach (JToken message in (IEnumerable)messages) 376 | { 377 | connection.OnReceived(message); 378 | } 379 | if ((int?)result["S"] == 1) 380 | { 381 | var responseResult = GetStartResponse(httpClient, _connection, _connectionData, Name).Result; 382 | if (responseResult == null) 383 | { 384 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 385 | } 386 | else 387 | { 388 | if (string.IsNullOrEmpty(responseResult)) 389 | { 390 | throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Error_ServerNegotiationFailed")); 391 | } 392 | else 393 | { 394 | var started = _connection.JsonDeserializeObject(responseResult)["Response"]; 395 | if (started.ToString() == "started") 396 | { 397 | return shouldReconnect; 398 | } 399 | else 400 | { 401 | OnStartFailed(); 402 | } 403 | } 404 | } 405 | } 406 | } 407 | } 408 | catch (Exception ex) 409 | { 410 | connection.OnError(ex); 411 | } 412 | return shouldReconnect; 413 | } 414 | 415 | public void Dispose() 416 | { 417 | Dispose(true); 418 | GC.SuppressFinalize(this); 419 | } 420 | 421 | private void Dispose(bool disposing) 422 | { 423 | if (disposing) 424 | { 425 | _finished = true; 426 | DisposeSocket(); 427 | } 428 | } 429 | } 430 | } -------------------------------------------------------------------------------- /XamarinChat/XamarinChat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Xamarin.Forms; 4 | 5 | namespace XamarinChat 6 | { 7 | public class App : Application 8 | { 9 | public static string SignalRUrl { get { return "http://192.168.0.8:62265/"; } } 10 | public App () 11 | { 12 | // The root page of your application 13 | MainPage = new ChatPage{BindingContext = new ChatViewModel()}; 14 | } 15 | 16 | protected override void OnStart () 17 | { 18 | // Handle when your app starts 19 | } 20 | 21 | protected override void OnSleep () 22 | { 23 | // Handle when your app sleeps 24 | } 25 | 26 | protected override void OnResume () 27 | { 28 | // Handle when your app resumes 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /XamarinChat/XamarinChat.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 7 | {86E41C61-C0E7-4355-AAA4-22063088FEF2} 8 | Library 9 | XamarinChat 10 | XamarinChat 11 | v4.5 12 | Profile78 13 | 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug 21 | DEBUG; 22 | prompt 23 | 4 24 | false 25 | 26 | 27 | full 28 | true 29 | bin\Release 30 | prompt 31 | 4 32 | false 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ..\packages\Microsoft.AspNet.SignalR.Client.2.2.2\lib\portable-net45+sl50+win+wpa81+wp80\Microsoft.AspNet.SignalR.Client.dll 57 | 58 | 59 | ..\packages\Newtonsoft.Json.10.0.3\lib\portable-net45+win8+wp8+wpa81\Newtonsoft.Json.dll 60 | 61 | 62 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.dll 63 | 64 | 65 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Extensions.dll 66 | 67 | 68 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Primitives.dll 69 | 70 | 71 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\portable-win+net45+wp80+win81+wpa81\Xamarin.Forms.Core.dll 72 | 73 | 74 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\portable-win+net45+wp80+win81+wpa81\Xamarin.Forms.Platform.dll 75 | 76 | 77 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\portable-win+net45+wp80+win81+wpa81\Xamarin.Forms.Xaml.dll 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /XamarinChat/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /XamarinChat/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /XamarinFormChatApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinChat", "XamarinChat\XamarinChat.csproj", "{86E41C61-C0E7-4355-AAA4-22063088FEF2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinChat.iOS", "iOS\XamarinChat.iOS.csproj", "{10864106-5667-487D-BCC1-CAB26FF8766A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamarinChat.Droid", "Droid\XamarinChat.Droid.csproj", "{EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|iPhone = Debug|iPhone 16 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 17 | Release|Any CPU = Release|Any CPU 18 | Release|iPhone = Release|iPhone 19 | Release|iPhoneSimulator = Release|iPhoneSimulator 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|iPhone.ActiveCfg = Debug|Any CPU 25 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|iPhone.Build.0 = Debug|Any CPU 26 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 27 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 28 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|iPhone.ActiveCfg = Release|Any CPU 31 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|iPhone.Build.0 = Release|Any CPU 32 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 33 | {86E41C61-C0E7-4355-AAA4-22063088FEF2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 34 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Debug|Any CPU.ActiveCfg = Debug|iPhone 35 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Debug|iPhone.ActiveCfg = Debug|iPhone 36 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Debug|iPhone.Build.0 = Debug|iPhone 37 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 38 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 39 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Release|Any CPU.ActiveCfg = Release|iPhone 40 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Release|iPhone.ActiveCfg = Release|iPhone 41 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Release|iPhone.Build.0 = Release|iPhone 42 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 43 | {10864106-5667-487D-BCC1-CAB26FF8766A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 44 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 47 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhone.ActiveCfg = Debug|Any CPU 48 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhone.Build.0 = Debug|Any CPU 49 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhone.Deploy.0 = Debug|Any CPU 50 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 51 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 52 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU 53 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|Any CPU.Deploy.0 = Release|Any CPU 56 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhone.ActiveCfg = Release|Any CPU 57 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhone.Build.0 = Release|Any CPU 58 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhone.Deploy.0 = Release|Any CPU 59 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 60 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 61 | {EABEFB5B-266E-4FC0-9F1E-0C43CCF43211}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 62 | EndGlobalSection 63 | GlobalSection(SolutionProperties) = preSolution 64 | HideSolutionNode = FALSE 65 | EndGlobalSection 66 | GlobalSection(ExtensibilityGlobals) = postSolution 67 | SolutionGuid = {2F4F759D-41F1-4E9B-888C-D5F36091D474} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Foundation; 6 | using UIKit; 7 | 8 | namespace XamarinChat.iOS 9 | { 10 | [Register ("AppDelegate")] 11 | public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 12 | { 13 | public override bool FinishedLaunching (UIApplication app, NSDictionary options) 14 | { 15 | global::Xamarin.Forms.Forms.Init (); 16 | 17 | LoadApplication (new App ()); 18 | 19 | return base.FinishedLaunching (app, options); 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /iOS/CustomRenderers/BubbleCell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using Foundation; 4 | using UIKit; 5 | using CoreGraphics; 6 | 7 | namespace XamarinChat.iOS 8 | { 9 | public class BubbleCell : UITableViewCell 10 | { 11 | public static NSString KeyLeft = new NSString("BubbleElementLeft"); 12 | public static NSString KeyRight = new NSString("BubbleElementRight"); 13 | public static UIImage bleft, bright, left, right; 14 | public static UIFont font = UIFont.SystemFontOfSize(14); 15 | UIView view; 16 | UIView imageView; 17 | UILabel label; 18 | bool isLeft; 19 | 20 | static BubbleCell() 21 | { 22 | bright = UIImage.FromFile("green.png"); 23 | bleft = UIImage.FromFile("grey.png"); 24 | 25 | // buggy, see https://bugzilla.xamarin.com/show_bug.cgi?id=6177 26 | //left = bleft.CreateResizableImage (new UIEdgeInsets (10, 16, 18, 26)); 27 | //right = bright.CreateResizableImage (new UIEdgeInsets (11, 11, 17, 18)); 28 | left = bleft.StretchableImage(26, 16); 29 | right = bright.StretchableImage(11, 11); 30 | } 31 | 32 | public BubbleCell(bool isLeft) 33 | : base(UITableViewCellStyle.Default, isLeft ? KeyLeft : KeyRight) 34 | { 35 | var rect = new RectangleF(0, 0, 1, 1); 36 | this.isLeft = isLeft; 37 | view = new UIView(rect); 38 | imageView = new UIImageView(isLeft ? left : right); 39 | view.AddSubview(imageView); 40 | label = new UILabel(rect) 41 | { 42 | LineBreakMode = UILineBreakMode.WordWrap, 43 | Lines = 0, 44 | Font = font, 45 | BackgroundColor = UIColor.Clear 46 | }; 47 | view.AddSubview(label); 48 | ContentView.Add(view); 49 | } 50 | 51 | public override void LayoutSubviews() 52 | { 53 | base.LayoutSubviews(); 54 | var frame = ContentView.Frame; 55 | var size = GetSizeForText(this, label.Text) + BubblePadding; 56 | imageView.Frame = new CGRect (isLeft ? 10 : frame.Width - size.Width - 10, frame.Y, size.Width,size.Height); 57 | // imageView.Frame = new RectangleF(new PointF(isLeft ? 10 : frame.Width - size.Width - 10, frame.Y), size); 58 | view.SetNeedsDisplay(); 59 | frame = imageView.Frame; 60 | var noBubbleSize = size - BubblePadding; 61 | label.Frame = new CGRect (frame.X + (isLeft ? 12 : 8), frame.Y + 6, noBubbleSize.Width,noBubbleSize.Height); 62 | // label.Frame = new RectangleF(new PointF(frame.X + (isLeft ? 12 : 8), frame.Y + 6), size - BubblePadding); 63 | } 64 | 65 | static internal SizeF BubblePadding = new SizeF(22, 16); 66 | 67 | static internal SizeF GetSizeForText(UIView tv, string text) 68 | { 69 | //return tv.StringSize(text, font, new SizeF(tv.Bounds.Width * .7f - 10 - 22, 99999)); 70 | 71 | NSString s = new NSString (text); 72 | var size = s.StringSize(font, new CGSize(tv.Bounds.Width * .7f - 10 - 22, 99999)); 73 | return new SizeF ((float)size.Width, (float)size.Height); 74 | } 75 | 76 | public void Update(string text) 77 | { 78 | label.Text = text; 79 | SetNeedsLayout(); 80 | } 81 | 82 | public float GetHeight(UIView tv) 83 | { 84 | return GetSizeForText(tv, label.Text).Height + BubblePadding.Height; 85 | } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /iOS/CustomRenderers/ChatBubble.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | using UIKit; 4 | using Foundation; 5 | using MonoTouch.Dialog; 6 | 7 | namespace XamarinChat.iOS 8 | { 9 | public class ChatBubble : MonoTouch.Dialog.Element, IElementSizing 10 | { 11 | bool isLeft; 12 | 13 | public ChatBubble(bool isLeft, string text) 14 | : base(text) 15 | { 16 | this.isLeft = isLeft; 17 | } 18 | 19 | 20 | public override UITableViewCell GetCell(UITableView tv) 21 | { 22 | var cell = tv.DequeueReusableCell(isLeft ? BubbleCell.KeyLeft : BubbleCell.KeyRight) as BubbleCell; 23 | if (cell == null) 24 | cell = new BubbleCell(isLeft); 25 | cell.Update(Caption); 26 | return cell; 27 | } 28 | 29 | public nfloat GetHeight(UITableView tableView, NSIndexPath indexPath) 30 | { 31 | return BubbleCell.GetSizeForText(tableView, Caption).Height + BubbleCell.BubblePadding.Height; 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /iOS/CustomRenderers/ChatListViewRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | using XamarinChat; 4 | using XamarinChat.iOS; 5 | using Xamarin.Forms.Platform.iOS; 6 | using UIKit; 7 | using Foundation; 8 | using System.Reflection; 9 | 10 | [assembly: ExportRenderer (typeof(ChatListView), typeof(ChatListViewRenderer))] 11 | namespace XamarinChat.iOS 12 | { 13 | public class ChatListViewRenderer : ListViewRenderer 14 | { 15 | protected override void OnElementChanged (ElementChangedEventArgs e) 16 | { 17 | base.OnElementChanged (e); 18 | var table = (UITableView)this.Control; 19 | table.SeparatorStyle = UITableViewCellSeparatorStyle.None; 20 | table.Source = new ListViewDataSourceWrapper (this.GetFieldValue (typeof(ListViewRenderer), "dataSource")); 21 | } 22 | } 23 | 24 | public class ListViewDataSourceWrapper : UITableViewSource 25 | { 26 | private readonly UITableViewSource _underlyingTableSource; 27 | 28 | public ListViewDataSourceWrapper (UITableViewSource underlyingTableSource) 29 | { 30 | this._underlyingTableSource = underlyingTableSource; 31 | } 32 | 33 | public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath) 34 | { 35 | return this.GetCellInternal (tableView, indexPath); 36 | } 37 | 38 | public override nint RowsInSection (UITableView tableview, nint section) 39 | { 40 | return this._underlyingTableSource.RowsInSection (tableview, section); 41 | } 42 | 43 | public override nfloat GetHeightForHeader (UITableView tableView, nint section) 44 | { 45 | return this._underlyingTableSource.GetHeightForHeader (tableView, section); 46 | } 47 | 48 | public override UIView GetViewForHeader (UITableView tableView, nint section) 49 | { 50 | return this._underlyingTableSource.GetViewForHeader (tableView, section); 51 | } 52 | 53 | public override nint NumberOfSections (UITableView tableView) 54 | { 55 | return this._underlyingTableSource.NumberOfSections (tableView); 56 | } 57 | 58 | public override void RowSelected (UITableView tableView, NSIndexPath indexPath) 59 | { 60 | this._underlyingTableSource.RowSelected (tableView, indexPath); 61 | } 62 | 63 | public override string[] SectionIndexTitles (UITableView tableView) 64 | { 65 | return this._underlyingTableSource.SectionIndexTitles (tableView); 66 | } 67 | 68 | public override string TitleForHeader (UITableView tableView, nint section) 69 | { 70 | return this._underlyingTableSource.TitleForHeader (tableView, section); 71 | } 72 | 73 | public override nfloat GetHeightForRow (UITableView tableView, NSIndexPath indexPath) 74 | { 75 | var uiCell = (BubbleCell)GetCellInternal (tableView, indexPath); 76 | 77 | uiCell.SetNeedsLayout (); 78 | uiCell.LayoutIfNeeded (); 79 | 80 | return uiCell.GetHeight (tableView); 81 | } 82 | 83 | private UITableViewCell GetCellInternal (UITableView tableView, NSIndexPath indexPath) 84 | { 85 | return this._underlyingTableSource.GetCell (tableView, indexPath); 86 | } 87 | } 88 | 89 | public static class PrivateExtensions 90 | { 91 | public static T GetFieldValue (this object @this, Type type, string name) 92 | { 93 | var field = type.GetField (name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField); 94 | return (T)field.GetValue (@this); 95 | } 96 | 97 | public static T GetPropertyValue (this object @this, Type type, string name) 98 | { 99 | var property = type.GetProperty (name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty); 100 | return (T)property.GetValue (@this); 101 | } 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /iOS/CustomRenderers/MessageRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms.Platform.iOS; 3 | using UIKit; 4 | using Xamarin.Forms; 5 | using XamarinChat; 6 | using XamarinChat.iOS; 7 | 8 | [assembly: ExportRenderer(typeof(MessageViewCell), typeof(MessageRenderer))] 9 | namespace XamarinChat.iOS 10 | { 11 | public class MessageRenderer : ViewCellRenderer 12 | { 13 | public override UITableViewCell GetCell (Cell item, UITableViewCell reusableCell, UITableView tv) 14 | { 15 | var textVm = item.BindingContext as ChatMessageViewModel; 16 | if (textVm != null) 17 | { 18 | string text = (textVm.IsMine ? "Me" : textVm.Name) + ": " + textVm.Message; 19 | var chatBubble = new ChatBubble(!textVm.IsMine, text); 20 | return chatBubble.GetCell(tv); 21 | } 22 | return base.GetCell(item,reusableCell, tv); 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /iOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | WebSocketXamarinChat 7 | CFBundleIdentifier 8 | com.edgarperez.websocketxamarinchat 9 | CFBundleShortVersionString 10 | 1.0 11 | CFBundleVersion 12 | 1.0 13 | LSRequiresIPhoneOS 14 | 15 | MinimumOSVersion 16 | 7.0 17 | UIDeviceFamily 18 | 19 | 1 20 | 2 21 | 22 | UILaunchStoryboardName 23 | LaunchScreen 24 | UIRequiredDeviceCapabilities 25 | 26 | armv7 27 | 28 | UISupportedInterfaceOrientations 29 | 30 | UIInterfaceOrientationPortrait 31 | UIInterfaceOrientationLandscapeLeft 32 | UIInterfaceOrientationLandscapeRight 33 | 34 | UISupportedInterfaceOrientations~ipad 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationPortraitUpsideDown 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | XSAppIconAssets 42 | Resources/Images.xcassets/AppIcons.appiconset 43 | 44 | 45 | -------------------------------------------------------------------------------- /iOS/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Foundation; 6 | using UIKit; 7 | 8 | namespace XamarinChat.iOS 9 | { 10 | public class Application 11 | { 12 | // This is the main entry point of the application. 13 | static void Main (string[] args) 14 | { 15 | // if you want to use a different Application Delegate class from "AppDelegate" 16 | // you can specify it here. 17 | UIApplication.Main (args, null, "AppDelegate"); 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /iOS/Resources/Images.xcassets/AppIcons.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "29x29", 5 | "scale": "1x", 6 | "idiom": "iphone" 7 | }, 8 | { 9 | "size": "29x29", 10 | "scale": "2x", 11 | "idiom": "iphone" 12 | }, 13 | { 14 | "size": "29x29", 15 | "scale": "3x", 16 | "idiom": "iphone" 17 | }, 18 | { 19 | "size": "40x40", 20 | "scale": "2x", 21 | "idiom": "iphone" 22 | }, 23 | { 24 | "size": "40x40", 25 | "scale": "3x", 26 | "idiom": "iphone" 27 | }, 28 | { 29 | "size": "57x57", 30 | "scale": "1x", 31 | "idiom": "iphone" 32 | }, 33 | { 34 | "size": "57x57", 35 | "scale": "2x", 36 | "idiom": "iphone" 37 | }, 38 | { 39 | "size": "60x60", 40 | "scale": "2x", 41 | "idiom": "iphone" 42 | }, 43 | { 44 | "size": "60x60", 45 | "scale": "3x", 46 | "idiom": "iphone" 47 | }, 48 | { 49 | "size": "29x29", 50 | "scale": "1x", 51 | "idiom": "ipad" 52 | }, 53 | { 54 | "size": "29x29", 55 | "scale": "2x", 56 | "idiom": "ipad" 57 | }, 58 | { 59 | "size": "40x40", 60 | "scale": "1x", 61 | "idiom": "ipad" 62 | }, 63 | { 64 | "size": "40x40", 65 | "scale": "2x", 66 | "idiom": "ipad" 67 | }, 68 | { 69 | "size": "50x50", 70 | "scale": "1x", 71 | "idiom": "ipad" 72 | }, 73 | { 74 | "size": "50x50", 75 | "scale": "2x", 76 | "idiom": "ipad" 77 | }, 78 | { 79 | "size": "72x72", 80 | "scale": "1x", 81 | "idiom": "ipad" 82 | }, 83 | { 84 | "size": "72x72", 85 | "scale": "2x", 86 | "idiom": "ipad" 87 | }, 88 | { 89 | "size": "76x76", 90 | "scale": "1x", 91 | "idiom": "ipad" 92 | }, 93 | { 94 | "size": "76x76", 95 | "scale": "2x", 96 | "idiom": "ipad" 97 | }, 98 | { 99 | "size": "120x120", 100 | "scale": "1x", 101 | "idiom": "car" 102 | } 103 | ], 104 | "info": { 105 | "version": 1, 106 | "author": "xcode" 107 | } 108 | } -------------------------------------------------------------------------------- /iOS/Resources/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /iOS/Resources/empty_contact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/iOS/Resources/empty_contact.jpg -------------------------------------------------------------------------------- /iOS/Resources/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/iOS/Resources/green.png -------------------------------------------------------------------------------- /iOS/Resources/grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/iOS/Resources/grey.png -------------------------------------------------------------------------------- /iOS/Resources/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgarleonardo/ChatAppXamarinFormWithWebSocket/68814a711f46833fa0caab87ddd8efe56ceb1f0d/iOS/Resources/group.png -------------------------------------------------------------------------------- /iOS/WebSocketImplementation/WebSocketImplementation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Text; 7 | using System.Collections.Generic; 8 | using XamarinChat.WebSocketImplementation; 9 | using System.Net; 10 | 11 | [assembly: Xamarin.Forms.Dependency(typeof(WebSocketImplementation))] 12 | namespace XamarinChat.WebSocketImplementation 13 | { 14 | public class WebSocketImplementation : IWebSocket 15 | { 16 | public ClientWebSocket WebSocket { get; set; } 17 | public Queue QueueOfSends { get; set; } 18 | public CancellationToken DisconectToken { get; set; } 19 | public Action ResolveWebSocketResponse { get; set; } 20 | public IConnectionHolder Connection { get; set; } 21 | public CookieContainer CookiesContainer 22 | { get { return WebSocket.Options.Cookies; } set { WebSocket.Options.Cookies = value; } } 23 | public WebSocketImplementation() 24 | { 25 | WebSocket = new ClientWebSocket(); 26 | QueueOfSends = new Queue(); 27 | } 28 | public async Task ConnectAsync(Uri uri, CancellationToken _disconnectToken) 29 | { 30 | try 31 | { 32 | await Task.Factory.StartNew(async () => 33 | { 34 | var uniInfo = uri; 35 | var disconnectToken = _disconnectToken; 36 | while (true) 37 | { 38 | if (IsConnected) 39 | { 40 | await ReadMessage(); 41 | await SendMessage(); 42 | } 43 | else 44 | { 45 | await WebSocket.ConnectAsync(uniInfo, disconnectToken); 46 | } 47 | } 48 | }, _disconnectToken, TaskCreationOptions.LongRunning, TaskScheduler.Default); 49 | } 50 | catch (Exception ex) 51 | { 52 | var mes = ex.Message; 53 | } 54 | } 55 | private async Task SendMessage() 56 | { 57 | try 58 | { 59 | do 60 | { 61 | if (WebSocket.State != WebSocketState.Open) 62 | { 63 | // Make this a faulted task and trigger the OnError even to maintain consistency with the HttpBasedTransports 64 | var ex = new InvalidOperationException("Error_DataCannotBeSentDuringWebSocketReconnect"); 65 | Connection.OnError(ex); 66 | return; 67 | } 68 | var message = ""; 69 | QueueOfSends.TryDequeue(out message); 70 | if (string.IsNullOrEmpty(message) && !CanSendMessage(message)) 71 | return; 72 | 73 | var byteMessage = Encoding.UTF8.GetBytes(message); 74 | var segmnet = new ArraySegment(byteMessage); 75 | 76 | await WebSocket.SendAsync(segmnet, WebSocketMessageType.Text, true, DisconectToken); 77 | } while (QueueOfSends.Count > 0); 78 | } 79 | catch (Exception ex) 80 | { 81 | var men = ex.Message; 82 | } 83 | } 84 | private async Task ReadMessage() 85 | { 86 | try 87 | { 88 | WebSocketReceiveResult result; 89 | string receivedMessage = ""; 90 | var message = new ArraySegment(new byte[4096]); 91 | do 92 | { 93 | result = await WebSocket.ReceiveAsync(message, DisconectToken); 94 | if (result.MessageType != WebSocketMessageType.Text) 95 | break; 96 | var messageBytes = message.Skip(message.Offset).Take(result.Count).ToArray(); 97 | receivedMessage += Encoding.UTF8.GetString(messageBytes); 98 | } 99 | while (!result.EndOfMessage); 100 | if (receivedMessage != "{}" && !string.IsNullOrEmpty(receivedMessage)) 101 | { 102 | ResolveWebSocketResponse.Invoke(receivedMessage); 103 | Console.WriteLine("Received: {0}", receivedMessage); 104 | } 105 | } 106 | catch (Exception ex) 107 | { 108 | var mes = ex.Message; 109 | } 110 | } 111 | public Task SetRequestHeader(string key, string value) 112 | { 113 | //Connection.Headers.Add(key, value); 114 | Connection.AddHeader(key, value); 115 | return Task.FromResult(0); 116 | } 117 | bool CanSendMessage(string message) 118 | { 119 | return IsConnected && !string.IsNullOrEmpty(message); 120 | } 121 | public bool IsConnected => WebSocket.State == WebSocketState.Open; 122 | public bool IsAborted => (WebSocket.State == WebSocketState.Aborted || WebSocket.State == WebSocketState.Closed); 123 | public Task SendMessageAsync(string message) 124 | { 125 | QueueOfSends.Enqueue(message); 126 | return Task.FromResult(0); 127 | } 128 | 129 | public void DisposeSocket() 130 | { 131 | //ResolveWebSocketResponse = null; 132 | } 133 | 134 | public void AddHeader(string key, string value) 135 | { 136 | WebSocket.Options.SetRequestHeader(key, value); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /iOS/XamarinChat.iOS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | iPhoneSimulator 7 | {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | {10864106-5667-487D-BCC1-CAB26FF8766A} 9 | Exe 10 | XamarinChat.iOS 11 | Resources 12 | XamarinChat.iOS 13 | 14 | 15 | publish\ 16 | true 17 | Disk 18 | false 19 | Foreground 20 | 7 21 | Days 22 | false 23 | false 24 | true 25 | 0 26 | 1.0.0.%2a 27 | false 28 | false 29 | true 30 | 31 | 32 | true 33 | full 34 | false 35 | bin\iPhoneSimulator\Debug 36 | DEBUG;ENABLE_TEST_CLOUD; 37 | prompt 38 | 4 39 | false 40 | i386 41 | None 42 | true 43 | true 44 | true 45 | iPhone Developer 46 | true 47 | 48 | 49 | full 50 | true 51 | bin\iPhone\Release 52 | prompt 53 | 4 54 | false 55 | ARMv7, ARM64 56 | Entitlements.plist 57 | true 58 | true 59 | iPhone Developer 60 | true 61 | 62 | 63 | full 64 | true 65 | bin\iPhoneSimulator\Release 66 | prompt 67 | 4 68 | false 69 | i386 70 | None 71 | true 72 | iPhone Developer 73 | true 74 | 75 | 76 | true 77 | full 78 | false 79 | bin\iPhone\Debug 80 | DEBUG;ENABLE_TEST_CLOUD; 81 | prompt 82 | 4 83 | false 84 | ARMv7, ARM64 85 | Entitlements.plist 86 | true 87 | true 88 | true 89 | true 90 | iPhone Developer 91 | true 92 | 93 | 94 | 95 | 96 | 97 | 98 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\Xamarin.iOS10\Xamarin.Forms.Core.dll 99 | 100 | 101 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\Xamarin.iOS10\Xamarin.Forms.Platform.dll 102 | 103 | 104 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\Xamarin.iOS10\Xamarin.Forms.Platform.iOS.dll 105 | 106 | 107 | ..\packages\Xamarin.Forms.2.5.0.91635\lib\Xamarin.iOS10\Xamarin.Forms.Xaml.dll 108 | 109 | 110 | 111 | 112 | 113 | 114 | {86E41C61-C0E7-4355-AAA4-22063088FEF2} 115 | XamarinChat 116 | 117 | 118 | 119 | 120 | false 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | False 145 | .NET Framework 3.5 SP1 146 | true 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /iOS/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /iOS/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------